class InputForm extends EventSystem { constructor(form_config, options = {}) { super(); this.uuid = guid(); this.form_config = form_config; this.form_data = {}; this.node = Interface.createElement('div', {class: 'form'}); this.max_label_width = 0; this.uses_wide_inputs = false; this.buildForm(); this.updateValues(true); } buildForm() { let jq_node = $(this.node); let scope = this; for (let form_id in this.form_config) { let input_config = this.form_config[form_id]; let data = this.form_data[form_id] = {}; form_id = form_id.replace(/"/g, ''); if (input_config === '_') { jq_node.append('
') } else { let bar = $(`
`) let label; if (typeof input_config.label == 'string') { label = Interface.createElement('label', {class: 'name_space_left', for: form_id}, tl(input_config.label)+((input_config.nocolon || !input_config.label)?'':':')) bar.append(label); if (!input_config.full_width && input_config.condition !== false) { this.max_label_width = Math.max(getStringWidth(label.textContent), this.max_label_width) } } if (input_config.full_width) { bar.addClass('full_width_dialog_bar'); this.uses_wide_inputs = true; } if (input_config.description) { bar.attr('title', tl(input_config.description)) } let input_element; if (['checkbox', 'buttons', 'color', 'info'].includes(input_config.type) == false) { this.uses_wide_inputs = true; } switch (input_config.type) { default: input_element = Object.assign(document.createElement('input'), { type: 'text', className: 'dark_bordered half focusable_input', id: form_id, value: input_config.value||'', placeholder: input_config.placeholder||'', oninput() { scope.updateValues() } }); bar.append(input_element) if (input_config.list) { let list_id = `${this.uuid}_${form_id}_list`; input_element.setAttribute('list', list_id); let list = $(``); for (let value of input_config.list) { let node = document.createElement('option'); node.value = value; list.append(node); } bar.append(list); } if (input_config.type == 'password') { bar.append(`
`) input_element.type = 'password'; let hidden = true; let this_bar = bar; let this_input_element = input_element; this_bar.find('.password_toggle').on('click', e => { hidden = !hidden; this_input_element.attributes.type.value = hidden ? 'password' : 'text'; this_bar.find('.password_toggle i')[0].className = hidden ? 'fas fa-eye-slash' : 'fas fa-eye'; }) } if (input_config.share_text && input_config.value) { let text = input_config.value.toString(); let is_url = text.startsWith('https://'); let copy_button = Interface.createElement('div', {class: 'form_input_tool tool', title: tl('dialog.copy_to_clipboard')}, Blockbench.getIconNode('content_paste')); copy_button.addEventListener('click', e => { if (isApp || navigator.clipboard) { Clipbench.setText(text); Blockbench.showQuickMessage('dialog.copied_to_clipboard'); input_element.focus(); document.execCommand('selectAll'); } else if (is_url) { Blockbench.showMessageBox({ title: 'dialog.share_model.title', message: `[${text}](${text})`, }) } }); bar.append(copy_button); if (is_url) { let open_button = Interface.createElement('div', {class: 'form_input_tool tool', title: tl('dialog.open_url')}, Blockbench.getIconNode('open_in_browser')); open_button.addEventListener('click', e => { Blockbench.openLink(text); }); bar.append(open_button); } if (navigator.share) { let share_button = Interface.createElement('div', {class: 'form_input_tool tool', title: tl('generic.share')}, Blockbench.getIconNode('share')); share_button.addEventListener('click', e => { navigator.share({ label: input_config.label ? tl(input_config.label) : 'Share', [is_url ? 'url' : 'text']: text }); }); bar.append(share_button); } } break; case 'textarea': input_element = Object.assign(document.createElement('textarea'), { className: 'focusable_input', id: form_id, value: input_config.value||'', placeholder: input_config.placeholder||'', oninput() { scope.updateValues() } }); input_element.style.height = (input_config.height || 150) + 'px'; bar.append(input_element) break; case 'select': let select_input = new Interface.CustomElements.SelectInput(form_id, { options: input_config.options, value: input_config.value || input_config.default, onInput() { scope.updateValues(); } }); data.select_input = select_input; bar.append(select_input.node) break; case 'inline_select': let options = []; let val = input_config.value || input_config.default; let i = 0; let wrapper; for (let key in input_config.options) { let is_selected = val ? key == val : i == 0; let text = input_config.options[key].name || input_config.options[key]; let node = Interface.createElement('li', {class: is_selected ? 'selected' : '', key: key}, tl(text)); node.onclick = event => { options.forEach(li => { li.classList.toggle('selected', li == node); }) scope.updateValues(); } options.push(node); i++; } wrapper = Interface.createElement('ul', {class: 'form_inline_select'}, options); bar.append(wrapper) break; case 'inline_multi_select': { let val = input_config.value || input_config.default; data.value = {}; if (val) { for (let key in input_config.options) { data.value[key] = !!val[key]; } } let i = 0; let options = []; let wrapper; for (let key in input_config.options) { let is_selected = val && val[key]; let text = input_config.options[key].name || input_config.options[key]; let node = Interface.createElement('li', {class: is_selected ? 'selected' : '', key: key}, tl(text)); node.onclick = event => { data.value[key] = !data.value[key]; node.classList.toggle('selected', data.value[key]); scope.updateValues(); } options.push(node); i++; } wrapper = Interface.createElement('ul', {class: 'form_inline_select multi_select'}, options); bar.append(wrapper) break; } case 'radio': let el = $(`
`) for (let key in input_config.options) { let name = tl(input_config.options[key]) el.append(`
`) input_element = el.find(`input#${key}`); input_element.on('change', () => { scope.updateValues() }) } bar.append(el) break; case 'info': data.text = pureMarked(tl(input_config.text)) bar.append(`

${data.text}

`) bar.addClass('small_text') break; case 'buttons': let list = document.createElement('div'); list.className = 'dialog_form_buttons'; input_config.buttons.forEach((button_text, index) => { let button = document.createElement('a'); button.innerText = tl(button_text); button.addEventListener('click', e => { input_config.click(index, e); }) list.append(button); }) bar.append(list); break; case 'number': let numeric_input = new Interface.CustomElements.NumericInput(form_id, { value: input_config.value, min: input_config.min, max: input_config.max, step: input_config.step, onChange() { scope.updateValues() } }); bar.append(numeric_input.node) break; case 'range': input_element = $(``) bar.append(input_element) if (!input_config.editable_range_label) { let display = Interface.createElement('span', {class: 'range_input_label'}, (input_config.value||0).toString()) bar.append(display); input_element.on('input', () => { let result = this.getResult(); display.textContent = trimFloatNumber(result[form_id]); }) } else { bar.addClass('slider_input_combo'); let numeric_input = new Interface.CustomElements.NumericInput(form_id + '_number', { value: input_config.value ?? 0, min: input_config.min, max: input_config.max, step: input_config.step, onChange() { input_element.val(numeric_input.value); scope.updateValues(); } }); bar.append(numeric_input.node); input_element.on('input', () => { let result = parseFloat(input_element.val()); numeric_input.value = result; }) } input_element.on('input', () => { scope.updateValues(); }) break; case 'num_slider': let getInterval = input_config.getInterval; if (input_config.interval_type == 'position') getInterval = getSpatialInterval; if (input_config.interval_type == 'rotation') getInterval = getRotationInterval; let slider = new NumSlider({ id: 'form_slider_'+form_id, private: true, onChange: () => { scope.updateValues(); }, getInterval, settings: { default: input_config.value || 0, min: input_config.min, max: input_config.max, step: input_config.step||1, }, }); bar.append(slider.node); slider.update(); data.slider = slider; break; case 'vector': let group = $(`
`) bar.append(group) let vector_inputs = []; let initial_value = input_config.value instanceof Array ? input_config.value.slice() : [1, 1, 1]; function updateInputs(changed_input) { let i2 = -1; for (let vector_input_2 of vector_inputs) { i2++; if (vector_input_2 == changed_input) continue; let new_value = initial_value[i2] * (changed_input.value / initial_value[vector_inputs.indexOf(changed_input)]); new_value = Math.clamp(new_value, input_config.min, input_config.max) if (input_config.force_step && input_config.step) { new_value = Math.round(new_value / input_config.step) * input_config.step; } vector_input_2.value = new_value; } } for (let i = 0; i < (input_config.dimensions || 3); i++) { let numeric_input = new Interface.CustomElements.NumericInput(form_id + '_' + i, { value: input_config.value ? input_config.value[i] : 0, min: input_config.min, max: input_config.max, step: input_config.step, onChange() { if (data.linked_ratio) { updateInputs(numeric_input); } scope.updateValues(); } }); group.append(numeric_input.node) vector_inputs.push(numeric_input); } if (typeof input_config.linked_ratio == 'boolean') { data.linked_ratio = input_config.linked_ratio; let icon = Blockbench.getIconNode('link'); let linked_ratio_toggle = Interface.createElement('div', {class: 'tool linked_ratio_toggle'}, icon); linked_ratio_toggle.addEventListener('click', event => { data.linked_ratio = !data.linked_ratio; if (data.linked_ratio) { initial_value = vector_inputs.map(v => v.value); // updateInputs(vector_inputs[0]); // scope.updateValues(); } updateState(); }) function updateState() { icon.textContent = data.linked_ratio ? 'link' : 'link_off'; linked_ratio_toggle.classList.toggle('enabled', data.linked_ratio); } updateState(); group.append(linked_ratio_toggle) } break; case 'color': if (input_config.colorpicker) data.colorpicker = input_config.colorpicker; if (!data.colorpicker) { data.colorpicker = new ColorPicker({ id: 'cp_'+form_id, name: tl(input_config.label), label: false, private: true, value: input_config.value }) } data.colorpicker.onChange = function() { scope.updateValues() }; bar.append(data.colorpicker.getNode()) break; case 'checkbox': input_element = $(``) bar.append(input_element) input_element.on('change', () => { scope.updateValues() }) break; case 'file': case 'folder': case 'save': if (input_config.type == 'folder' && !isApp) break; data.value = input_config.value; let input = $(``); input[0].value = settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : data.value || ''; let input_wrapper = $('
'); input_wrapper.append(input); bar.append(input_wrapper); bar.addClass('form_bar_file'); switch (input_config.type) { case 'file': input_wrapper.append('insert_drive_file'); break; case 'folder': input_wrapper.append('folder'); break; case 'save': input_wrapper.append('save'); break; } let remove_button = $('
clear
'); bar.append(remove_button); remove_button.on('click', e => { e.stopPropagation(); data.value = ''; delete data.content; delete data.file; input.val(''); }) input_wrapper.on('click', e => { function fileCB(files) { data.value = files[0].path; data.content = files[0].content; data.file = files[0]; input.val(settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : data.value); scope.updateValues() } switch (input_config.type) { case 'file': Blockbench.import({ resource_id: input_config.resource_id, extensions: input_config.extensions, type: input_config.filetype, startpath: data.value, readtype: input_config.readtype }, fileCB); break; case 'folder': let path = Blockbench.pickDirectory({ startpath: data.value, }) if (path) fileCB([{path}]); break; case 'save': Blockbench.export({ resource_id: input_config.resource_id, extensions: input_config.extensions, type: input_config.filetype, startpath: data.value, custom_writer: () => {}, }, path => { data.value = path; input.val(settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : data.value); scope.updateValues() }); break; } }) } if (input_config.readonly) { bar.find('input').attr('readonly', 'readonly').removeClass('focusable_input') } if (input_config.description) { let icon = document.createElement('i'); icon.className = 'fa fa-question dialog_form_description'; icon.onclick = function() { Blockbench.showQuickMessage(input_config.description, 3600); } bar.append(icon); } if (input_config.toggle_enabled) { let toggle = Interface.createElement('input', { type: 'checkbox', class: 'focusable_input form_input_toggle', id: form_id + '_toggle', }) toggle.checked = input_config.toggle_default != false; bar.append(toggle); bar.toggleClass('form_toggle_disabled', !toggle.checked); toggle.addEventListener('input', () => { scope.updateValues(); bar.toggleClass('form_toggle_disabled', !toggle.checked); }); data.input_toggle = toggle; } jq_node.append(bar) data.bar = bar; } } this.node.style.setProperty('--max_label_width', this.max_label_width+'px'); } updateValues(initial) { let form_result = this.getResult(); for (let form_id in this.form_config) { let data = this.form_data[form_id]; let input_config = this.form_config[form_id]; if (typeof input_config == 'object' && data.bar) { let show = Condition(input_config.condition, form_result); data.bar.toggle(show); } } if (!initial) { this.dispatchEvent('change', {result: form_result}); } return form_result; } setValues(values, update = true) { for (let form_id in this.form_config) { let data = this.form_data[form_id]; let input_config = this.form_config[form_id]; if (values[form_id] != undefined && typeof input_config == 'object' && data.bar) { let value = values[form_id]; switch (input_config.type) { default: data.bar.find('input').val(value); break; case 'info': break; case 'textarea': data.bar.find('textarea').val(value); break; case 'select': data.select_input.set(value); break; case 'inline_select': data.bar.find('li').each((i, el) => { el.classList.toggle('selected', el.getAttribute('key') == value); }) break; case 'inline_multi_select': for (let key in value) { if (data.value[key] !== undefined) { data.value[key] = value[key]; } } data.bar.find('li').each((i, el) => { el.classList.toggle('selected', !!data.value[el.getAttribute('key')]); }) break; case 'radio': data.bar.find('.form_part_radio input#'+value).prop('checked', value); break; case 'number': case 'range': data.bar.find('input').val(value); break; case 'num_slider': data.slider.setValue(value); break; case 'vector': for (let i = 0; i < (input_config.dimensions || 3); i++) { data.bar.find(`input#${form_id}_${i}`).val(value[i]) } break; case 'color': data.colorpicker.set(value); break; case 'checkbox': data.bar.find('input').prop('checked', value); break; case 'file': delete data.file; if (input_config.return_as == 'file' && typeof value == 'object') { data.file = value; data.value = data.file.name; } else if (isApp) { data.value = value; } else { data.content = value; } data.bar.find('input').val(settings.streamer_mode.value ? `[${tl('generic.redacted')}]` : data.value); break; } } } if (update) this.updateValues(); } setToggles(values, update = true) { for (let form_id in this.form_config) { let input_config = this.form_config[form_id]; let data = this.form_data[form_id]; if (values[form_id] != undefined && typeof input_config == 'object' && data.input_toggle && data.bar) { data.input_toggle.checked = values[form_id]; data.bar.toggleClass('form_toggle_disabled', !data.input_toggle.checked); } } if (update) this.updateValues(); } getResult() { let result = {} for (let form_id in this.form_config) { let input_config = this.form_config[form_id]; let data = this.form_data[form_id]; if (data && data.input_toggle && data.input_toggle.checked == false) { result[form_id] = null; continue; } if (typeof input_config === 'object') { switch (input_config.type) { default: result[form_id] = data.bar.find('input#'+form_id).val() break; case 'info': break; case 'textarea': result[form_id] = data.bar.find('textarea#'+form_id).val() break; case 'select': result[form_id] = data.bar.find('bb-select#'+form_id).attr('value'); break; case 'inline_select': result[form_id] = data.bar.find('li.selected')[0]?.getAttribute('key') || ''; break; case 'inline_multi_select': result[form_id] = data.value; break; case 'radio': result[form_id] = data.bar.find('.form_part_radio#'+form_id+' input:checked').attr('id') break; case 'number': result[form_id] = Math.clamp(parseFloat(data.bar.find('input#'+form_id).val())||0, input_config.min, input_config.max) if (input_config.force_step && input_config.step) { result[form_id] = Math.round(result[form_id] / input_config.step) * input_config.step; } break; case 'range': if (input_config.editable_range_label) { result[form_id] = Math.clamp(parseFloat(data.bar.find('input#'+form_id+'_number').val())||0, input_config.min, input_config.max); } else { result[form_id] = Math.clamp(parseFloat(data.bar.find('input#'+form_id).val())||0, input_config.min, input_config.max); } if (input_config.force_step && input_config.step) { result[form_id] = Math.round(result[form_id] / input_config.step) * input_config.step; } break; case 'num_slider': result[form_id] = data.slider.get(); break; case 'vector': result[form_id] = []; for (let i = 0; i < (input_config.dimensions || 3); i++) { let num = Math.clamp(parseFloat(data.bar.find(`input#${form_id}_${i}`).val())||0, input_config.min, input_config.max) if (input_config.force_step && input_config.step) { num = Math.round(num / input_config.step) * input_config.step; } result[form_id].push(num) } break; case 'color': result[form_id] = data.colorpicker.get(); break; case 'checkbox': result[form_id] = data.bar.find('input#'+form_id).is(':checked') break; case 'file': if (input_config.return_as == 'file') { result[form_id] = data.file; } else { result[form_id] = isApp ? data.value : data.content; } break; } } } return result; } static getDefaultValue(input_config) { let set_value = input_config.value ?? input_config.default; if (set_value) return set_value; switch (input_config.type) { case 'checkbox': return false; case 'text': case 'textarea': return ''; case 'number': case 'range': case 'num_slider': return Math.clamp(0, input_config.min, input_config.max); case 'select': case 'inline_select': case 'radio': return Object.keys(input_config.options)[0] ?? ''; case 'inline_multi_select': return {}; case 'file': case 'folder': return ''; case 'vector': return new Array(input_config.dimensions??3).fill(Math.clamp(0, input_config.min, input_config.max)); case 'color': return '#ffffff'; default: return ''; } } }