From c072caeb7db83d61faf12c4ee9783e5ae6d28cbf Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Sun, 23 Aug 2020 19:25:35 -0700 Subject: [PATCH] feat: custom option attributes resolves #1038, #986, #956 --- .eslintrc.json | 1 + src/demo/js/demo.js | 12 ++++-- src/js/control/select.js | 4 +- src/js/form-builder.js | 83 +++++++++++++++++++--------------------- src/js/form-render.js | 2 +- src/js/helpers.js | 24 +++++++----- src/js/layout.js | 2 +- src/sass/_stage.scss | 6 ++- 8 files changed, 72 insertions(+), 62 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index f3044ec6d..2f63f1f05 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,7 @@ "semi": ["error", "never"], "no-unused-vars": 1, "no-prototype-builtins": 0, + "arrow-parens": ["warn", "as-needed"], "prefer-const": [ 1, { diff --git a/src/demo/js/demo.js b/src/demo/js/demo.js index 9e2479aff..b330dde40 100644 --- a/src/demo/js/demo.js +++ b/src/demo/js/demo.js @@ -213,9 +213,10 @@ jQuery(function($) { onAddField: fieldId => { setCurrentFieldIdValues(fieldId) }, - onAddOption: (optionTemplate, optionIndex) => { - optionTemplate.label = `Option ${optionIndex + 1}` - optionTemplate.value = `option-${optionIndex + 1}` + onAddOption: (optionTemplate, {index}) => { + optionTemplate.label = optionTemplate.label || `Option ${index + 1}` + optionTemplate.value = optionTemplate.value || `option-${index + 1}` + return optionTemplate }, onClearAll: () => window.sessionStorage.removeItem('formData'), @@ -316,7 +317,10 @@ jQuery(function($) { demoApi.appendChild(generateActionTable(demoActions, columns)) if (formData && formData !== '[]') { - document.getElementById('set-form-data-value').value = window.JSON.stringify(JSON.parse(formData), null, ' ') + const setFormDataInputValue = document.getElementById('set-form-data-value') + if (setFormDataInputValue) { + setFormDataInputValue.value = window.JSON.stringify(JSON.parse(formData), null, ' ') + } } langSelect.addEventListener( diff --git a/src/js/control/select.js b/src/js/control/select.js index ca09ec55b..c37969d9a 100755 --- a/src/js/control/select.js +++ b/src/js/control/select.js @@ -1,4 +1,5 @@ import control from '../control' +import { trimObj } from '../utils' /** * Text input class @@ -134,7 +135,7 @@ export default class controlSelect extends control { // build & return the DOM elements if (type == 'select') { - this.dom = this.markup(optionType, options, data) + this.dom = this.markup(optionType, options, trimObj(data, true)) } else { this.dom = this.markup('div', options, { className: type }) } @@ -194,7 +195,6 @@ export default class controlSelect extends control { return } - // let foundMatch = false for (let i = 0; i < selectedOptions.length; i++) { if (input.value === selectedOptions[i]) { input.setAttribute('checked', true) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 8467ab80b..e8fc6dd5e 100755 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -24,6 +24,7 @@ import { closest, safename, forceNumber, + getContentType } from './utils' import { css_prefix_text } from '../fonts/config.json' @@ -271,23 +272,16 @@ const FormBuilder = function(opts, element, $) { * @return {String} field options markup */ const fieldOptions = function(fieldData) { - const { type, values, name } = fieldData + const { type, values } = fieldData let fieldValues const optionActions = [m('a', mi18n.get('addOption'), { className: 'add add-opt' })] const fieldOptions = [m('label', mi18n.get('selectOptions'), { className: 'false-label' })] const isMultiple = fieldData.multiple || type === 'checkbox-group' - const optionDataTemplate = label => { - const optionData = { - label, - value: hyphenCase(label), - } - - if (type !== 'autocomplete') { - optionData.selected = false - } - - return optionData - } + const optionDataTemplate = label => ({ + selected: false, + label, + value: hyphenCase(label), + }) if (!values || !values.length) { let defaultOptCount = [1, 2, 3] @@ -308,7 +302,9 @@ const FormBuilder = function(opts, element, $) { const optionActionsWrap = m('div', optionActions, { className: 'option-actions' }) const options = m( 'ol', - fieldValues.map(option => selectFieldOptions(name, option, isMultiple)), + fieldValues.map((option, index) => { + const optionData = config.opts.onAddOption(option, {type, index, isMultiple}) + return selectFieldOptions(optionData, isMultiple)}), { className: 'sortable-options', }, @@ -512,11 +508,10 @@ const FormBuilder = function(opts, element, $) { /** * Detects the type of user defined attribute - * @param {String} attr attribute name * @param {Object} attrData attribute config * @return {String} type of user attr */ - function userAttrType(attr, attrData) { + function userAttrType(attrData) { return ( [ ['array', ({ options }) => !!options], @@ -553,7 +548,7 @@ const FormBuilder = function(opts, element, $) { for (const attribute in typeUserAttr) { if (typeUserAttr.hasOwnProperty(attribute)) { - const attrValType = userAttrType(attribute, typeUserAttr[attribute]) + const attrValType = userAttrType(typeUserAttr[attribute]) const orig = mi18n.get(attribute) const tUA = typeUserAttr[attribute] const origValue = tUA.value || '' @@ -974,35 +969,37 @@ const FormBuilder = function(opts, element, $) { } // Select field html, since there may be multiple - const selectFieldOptions = function(name, optionData, multipleSelect) { + const selectFieldOptions = function(optionData, multipleSelect) { + const optionTemplate = { selected: false, label: '', value: '' } const optionInputType = { selected: multipleSelect ? 'checkbox' : 'radio', } - const optionDataOrder = ['value', 'label', 'selected'] - const optionInputs = [] - const optionTemplate = { selected: false, label: '', value: '' } - - optionData = Object.assign(optionTemplate, optionData) - - for (let i = optionDataOrder.length - 1; i >= 0; i--) { - const prop = optionDataOrder[i] - if (optionData.hasOwnProperty(prop)) { - const attrs = { - type: optionInputType[prop] || 'text', - className: 'option-' + prop, - value: optionData[prop], - name: name + '-option', + const optionInputTypeMap = { + boolean: (value, prop) => { + const attrs = {value, type: optionInputType[prop] || 'checkbox'} + if (value) { + attrs.checked = !!value } + return['input', null, attrs] + }, + number: value => ['input', null, {value, type: 'number'}], + string: (value, prop) => (['input', null, {value, type: 'text', placeholder: mi18n.get(`placeholder.${prop}`) || ''}]), + array: values => ['select', values.map(({label, value}) => m('option', label, {value}))], + object: ({tag, content, ...attrs}) => [tag, content, attrs], + } - attrs.placeholder = mi18n.get(`placeholder.${prop}`) || '' + optionData = {...optionTemplate, ...optionData} - if (prop === 'selected' && optionData.selected === true) { - attrs.checked = optionData.selected - } + const optionInputs = Object.entries(optionData).map(([prop, val]) => { + const optionInputDataType = getContentType(val) - optionInputs.push(m('input', null, attrs)) - } - } + const [tag, content, attrs] = optionInputTypeMap[optionInputDataType](val, prop) + const optionClassName = `option-${prop} option-attr` + attrs['data-attr'] = prop + attrs.className = attrs.className ? `${attrs.className} ${optionClassName}` : optionClassName + + return m(tag, content, attrs) + }) const removeAttrs = { className: `remove btn ${css_prefix_text}cancel`, @@ -1296,6 +1293,7 @@ const FormBuilder = function(opts, element, $) { // Attach a callback to add new options $stage.on('click', '.add-opt', function(e) { e.preventDefault() + const type = $(e.target).closest('.form-field').attr('type') const $optionWrap = $(e.target).closest('.field-options') const $multiple = $('[name="multiple"]', $optionWrap) const $firstOption = $('.option-selected:eq(0)', $optionWrap) @@ -1307,11 +1305,10 @@ const FormBuilder = function(opts, element, $) { isMultiple = $firstOption.attr('type') === 'checkbox' } - const name = $firstOption.attr('name').replace(/-option$/, '') - const optionTemplate = { selected: false, label: '', value: '' } - const optionData = config.opts.onAddOption(optionTemplate, $('.sortable-options', $optionWrap).children().length) - $('.sortable-options', $optionWrap).append(selectFieldOptions(name, optionData, isMultiple)) + const $sortableOptions = $('.sortable-options', $optionWrap) + const optionData = config.opts.onAddOption(optionTemplate, {type, index: $sortableOptions.children().length, isMultiple}) + $sortableOptions.append(selectFieldOptions(optionData, isMultiple)) }) $stage.on('mouseover mouseout', '.remove, .del-button', e => $(e.target).closest('li').toggleClass('delete')) diff --git a/src/js/form-render.js b/src/js/form-render.js index b6ccf8539..49d86527b 100755 --- a/src/js/form-render.js +++ b/src/js/form-render.js @@ -272,7 +272,7 @@ class FormRender { .filter(fieldData => fieldData.subtype === 'tinymce') .forEach(fieldData => window.tinymce.get(fieldData.name).save()) - this.instanceContainers.forEach((container) => { + this.instanceContainers.forEach(container => { const userDataMap = $('select, input, textarea', container) .serializeArray() .reduce((acc, { name, value }) => { diff --git a/src/js/helpers.js b/src/js/helpers.js index 9b0a9a99a..f8ec12682 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -129,16 +129,22 @@ export default class Helpers { const $options = $('.sortable-options li', field) $options.each(i => { - const $option = $($options[i]) - const selected = $('.option-selected', $option).is(':checked') - const attrs = { - label: $('.option-label', $option).val(), - value: $('.option-value', $option).val(), - } + const option = $options[i] + const stringAttrs = option.querySelectorAll('input[type=text], input[type=number], select') + const boolAttrs = option.querySelectorAll('input[type=checkbox], input[type=radio]') + const attrs = {} + + forEach(stringAttrs, i => { + const stringAttr = stringAttrs[i] + const attrName = stringAttr.dataset.attr + attrs[attrName] = stringAttr.value + }) - if (selected) { - attrs.selected = selected - } + forEach(boolAttrs, i => { + const boolAttr = boolAttrs[i] + const attrName = boolAttr.getAttribute('data-attr') + attrs[attrName] = boolAttr.checked + }) options.push(attrs) }) diff --git a/src/js/layout.js b/src/js/layout.js index 4b10b2655..43271f821 100755 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -58,7 +58,7 @@ export default class layout { className: processClassName(data, field) }) }, - hidden: (field) => { + hidden: field => { // no wrapper any any visible elements return field } diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index d9593ef70..a61d977fe 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -431,8 +431,10 @@ padding: 0; > li { + display: flex; cursor: move; margin: 1px; + padding-right: 28px; &:nth-child(1) .remove { display: none; @@ -447,7 +449,7 @@ .remove { position: absolute; opacity: 1; - right: 14px; + right: 8px; height: 18px; width: 18px; top: 14px; @@ -473,7 +475,7 @@ input[type='text'] { width: calc(44.5% - 17px); - margin: 0 1%; + margin: 0 3px; float: none; } }