%PDF- %PDF-
Direktori : /home/opti/workflow/src/public/app-assets/vendors/formBuilder-master/src/js/ |
Current File : /home/opti/workflow/src/public/app-assets/vendors/formBuilder-master/src/js/form-builder.js |
import '../sass/form-builder.scss' import throttle from 'lodash/throttle' import Dom from './dom' import { remove } from './dom' import { Data } from './data' import mi18n from 'mi18n' import events from './events' import layout from './layout' import Helpers from './helpers' import { defaultOptions, defaultI18n, config, styles } from './config' import Controls from './controls' import { subtract, hyphenCase, nameAttr, trimObj, forEach, markup, removeFromArray, attrString, capitalize, parsedHtml, addEventListeners, closest, safename, forceNumber, } from './utils' const DEFAULT_TIMEOUT = 333 const FormBuilder = function(opts, element) { const formBuilder = this const i18n = mi18n.current const formID = `frmb-${new Date().getTime()}` const data = new Data(formID) const d = new Dom(formID) // prepare a new layout object with appropriate templates if (!opts.layout) { opts.layout = layout } const layoutEngine = new opts.layout(opts.layoutTemplates, true) const h = new Helpers(formID, layoutEngine, formBuilder) const m = markup opts = h.processOptions(opts) data.layout = h.editorLayout(opts.controlPosition) h.editorUI(formID) data.formID = formID data.lastID = `${data.formID}-fld-1` const controls = new Controls(opts, d) const subtypes = (config.subtypes = h.processSubtypes(opts.subtypes)) const $stage = $(d.stage) const $cbUL = $(d.controls) // Sortable fields $stage.sortable({ cursor: 'move', opacity: 0.9, revert: 150, beforeStop: (evt, ui) => h.beforeStop.call(h, evt, ui), start: (evt, ui) => h.startMoving.call(h, evt, ui), stop: (evt, ui) => h.stopMoving.call(h, evt, ui), cancel: ['input', 'select', 'textarea', '.disabled-field', '.form-elements', '.btn', 'button', '.is-locked'].join(', '), placeholder: 'frmb-placeholder', }) if (!opts.allowStageSort) { $stage.sortable('disable') } // ControlBox with different fields $cbUL.sortable({ helper: 'clone', opacity: 0.9, connectWith: $stage, cancel: '.fb-separator', cursor: 'move', scroll: false, placeholder: 'ui-state-highlight', start: (evt, ui) => h.startMoving.call(h, evt, ui), stop: (evt, ui) => h.stopMoving.call(h, evt, ui), revert: 150, beforeStop: (evt, ui) => h.beforeStop.call(h, evt, ui), distance: 3, update: function(event, ui) { if (h.doCancel) { return false } if (ui.item.parent()[0] === d.stage) { h.doCancel = true processControl(ui.item) } else { h.setFieldOrder($cbUL) h.doCancel = !opts.sortableControls } }, }) const processControl = control => { if (control[0].classList.contains('input-set-control')) { const inputSets = [] const inputSet = opts.inputSets.find(set => hyphenCase(set.name || set.label) === control[0].dataset.type) if (inputSet && inputSet.showHeader) { const header = { type: 'header', subtype: 'h2', id: inputSet.name, label: inputSet.label, } inputSets.push(header) } inputSets.push(...inputSet.fields) inputSets.forEach(field => { prepFieldVars(field, true) if (h.stopIndex || h.stopIndex === 0) { h.stopIndex++ } }) } else { prepFieldVars(control, true) } } const $editorWrap = $(d.editorWrap) const cbWrap = m('div', d.controls, { id: `${data.formID}-cb-wrap`, className: `cb-wrap ${data.layout.controls}`, }) if (opts.showActionButtons) { cbWrap.appendChild(d.formActions) } $editorWrap.append(d.stage, cbWrap) if (element.type !== 'textarea') { $(element).append($editorWrap) } else { // formBuilder no longer uses textArea for element $(element).replaceWith($editorWrap) } $(d.controls).on('click', 'li', ({ target }) => { const $control = $(target).closest('li') h.stopIndex = undefined processControl($control) h.save.call(h) }) // Add append and prepend options if necessary const nonEditableFields = () => { const cancelArray = [] const disabledField = type => m('li', opts[type], { className: `disabled-field form-${type}`, }) if (opts.prepend && !$('.disabled-field.form-prepend', d.stage).length) { cancelArray.push(true) $stage.prepend(disabledField('prepend')) } if (opts.append && !$('.disabled-field.form-.append', d.stage).length) { cancelArray.push(true) $stage.append(disabledField('append')) } h.disabledTT(d.stage) return cancelArray.some(elem => elem === true) } // builds the standard formbuilder datastructure for a field definition const prepFieldVars = function($field, isNew = false) { let field = {} if ($field instanceof jQuery) { // get the default type etc & label for this field field.type = $field[0].dataset.type if (field.type) { // check for a custom type const custom = controls.custom.lookup(field.type) if (custom) { field = Object.assign({}, custom) } else { const controlClass = controls.getClass(field.type) field.label = controlClass.label(field.type) } // @todo: any other attrs ever set in aFields? value or selected? } else { // is dataType XML const attrs = $field[0].attributes if (!isNew) { field.values = $field.children().map((index, elem) => { return { label: $(elem).text(), value: $(elem).attr('value'), selected: Boolean($(elem).attr('selected')), } }) } for (let i = attrs.length - 1; i >= 0; i--) { field[attrs[i].name] = attrs[i].value } } } else { field = Object.assign({}, $field) } if (!field.name) { field.name = nameAttr(field) } if (isNew && ['text', 'number', 'file', 'date', 'select', 'textarea', 'autocomplete'].includes(field.type)) { field.className = field.className || 'form-control' } const match = /(?:^|\s)btn-(.*?)(?:\s|$)/g.exec(field.className) if (match) { field.style = match[1] } if (isNew) { field = Object.assign({}, field, opts.onAddField(data.lastID, field)) setTimeout(() => document.dispatchEvent(events.fieldAdded), 10) } appendNewField(field, isNew) d.stage.classList.remove('empty') } // Parse saved XML template data const loadFields = function(formData) { formData = h.getData(formData) if (formData && formData.length) { formData.forEach(fieldData => prepFieldVars(trimObj(fieldData))) d.stage.classList.remove('empty') } else if (opts.defaultFields && opts.defaultFields.length) { // Load default fields if none are set opts.defaultFields.forEach(field => prepFieldVars(field)) d.stage.classList.remove('empty') } else if (!opts.prepend && !opts.append) { d.stage.classList.add('empty') d.stage.dataset.content = mi18n.get('getStarted') } if (nonEditableFields()) { d.stage.classList.remove('empty') } h.save() } /** * Add data for field with options [select, checkbox-group, radio-group] * * @param {Object} fieldData * @return {String} field options markup */ const fieldOptions = function(fieldData) { const { type, values, name } = 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 } if (!values || !values.length) { let defaultOptCount = [1, 2, 3] if (['checkbox-group', 'checkbox'].includes(type)) { defaultOptCount = [1] } fieldValues = defaultOptCount.map(index => optionDataTemplate(`${mi18n.get('optionCount', index)}`)) const firstOption = fieldValues[0] if (firstOption.hasOwnProperty('selected') && type !== 'radio-group') { firstOption.selected = true } } else { // ensure option data is has all required keys fieldValues = values.map(option => Object.assign({}, { selected: false }, option)) } const optionActionsWrap = m('div', optionActions, { className: 'option-actions' }) const options = m('ol', fieldValues.map(option => selectFieldOptions(name, option, isMultiple)), { className: 'sortable-options', }) const optionsWrap = m('div', [options, optionActionsWrap], { className: 'sortable-options-wrap' }) fieldOptions.push(optionsWrap) return m('div', fieldOptions, { className: 'form-group field-options' }).outerHTML } const defaultFieldAttrs = type => { const defaultAttrs = ['required', 'label', 'description', 'placeholder', 'className', 'name', 'access', 'value'] const noValFields = ['header', 'paragraph', 'file', 'autocomplete'].concat(d.optionFields) const valueField = !noValFields.includes(type) const typeAttrsMap = { autocomplete: defaultAttrs.concat(['options', 'requireValidOption']), button: ['label', 'subtype', 'style', 'className', 'name', 'value', 'access'], checkbox: [ 'required', 'label', 'description', 'toggle', 'inline', 'className', 'name', 'access', 'other', 'options', ], text: defaultAttrs.concat(['subtype', 'maxlength']), date: defaultAttrs, file: defaultAttrs.concat(['subtype', 'multiple']), header: ['label', 'subtype', 'className', 'access'], hidden: ['name', 'value', 'access'], paragraph: ['label', 'subtype', 'className', 'access'], number: defaultAttrs.concat(['min', 'max', 'step']), select: defaultAttrs.concat(['multiple', 'options']), textarea: defaultAttrs.concat(['subtype', 'maxlength', 'rows']), } typeAttrsMap['checkbox-group'] = typeAttrsMap.checkbox typeAttrsMap['radio-group'] = typeAttrsMap.checkbox const typeAttrs = typeAttrsMap[type] if (type === 'radio-group') { removeFromArray('toggle', typeAttrs) } // Help Text / Description Field if (['header', 'paragraph', 'button'].includes(type)) { removeFromArray('description', typeAttrs) } if (!valueField) { removeFromArray('value', typeAttrs) } return typeAttrs || defaultAttrs } /** * Build the editable properties for the field * @param {object} values configuration object for advanced fields * @return {String} markup for advanced fields */ const advFields = values => { const { type } = values const advFields = [] const fieldAttrs = defaultFieldAttrs(type) const advFieldMap = { required: () => requiredField(values), toggle: () => boolAttribute('toggle', values, { first: mi18n.get('toggle') }), inline: () => { const labels = { first: mi18n.get('inline'), second: mi18n.get('inlineDesc', type.replace('-group', '')), } return boolAttribute('inline', values, labels) }, label: () => textAttribute('label', values), description: () => textAttribute('description', values), subtype: () => selectAttribute('subtype', values, subtypes[type]), style: () => btnStyles(values.style), placeholder: () => textAttribute('placeholder', values), rows: () => numberAttribute('rows', values), className: isHidden => textAttribute('className', values, isHidden), name: isHidden => textAttribute('name', values, isHidden), value: () => textAttribute('value', values), maxlength: () => numberAttribute('maxlength', values), access: () => { const rolesDisplay = values.role ? 'style="display:block"' : '' const availableRoles = [`<div class="available-roles" ${rolesDisplay}>`] for (key in opts.roles) { if (opts.roles.hasOwnProperty(key)) { const roleId = `fld-${data.lastID}-roles-${key}` const cbAttrs = { type: 'checkbox', name: 'roles[]', value: key, id: roleId, className: 'roles-field', } if (roles.includes(key)) { cbAttrs.checked = 'checked' } availableRoles.push(`<label for="${roleId}">`) availableRoles.push(h.input(cbAttrs).outerHTML) availableRoles.push(` ${opts.roles[key]}</label>`) } } availableRoles.push('</div>') const accessLabels = { first: mi18n.get('roles'), second: mi18n.get('limitRole'), content: availableRoles.join(''), } return boolAttribute('access', values, accessLabels) }, other: () => boolAttribute('other', values, { first: mi18n.get('enableOther'), second: mi18n.get('enableOtherMsg'), }), options: () => fieldOptions(values), requireValidOption: () => boolAttribute('requireValidOption', values, { first: ' ', second: mi18n.get('requireValidOption'), }), multiple: () => { const typeLabels = { default: { first: 'Multiple', second: 'set multiple attribute', }, file: { first: mi18n.get('multipleFiles'), second: mi18n.get('allowMultipleFiles'), }, select: { first: ' ', second: mi18n.get('selectionsMessage'), }, } return boolAttribute('multiple', values, typeLabels[type] || typeLabels.default) }, } let key const roles = values.role !== undefined ? values.role.split(',') : [] const numAttrs = ['min', 'max', 'step'] numAttrs.forEach(numAttr => { advFieldMap[numAttr] = () => numberAttribute(numAttr, values) }) const noDisable = ['name', 'className'] Object.keys(fieldAttrs).forEach(index => { const attr = fieldAttrs[index] const useDefaultAttr = [true] const isDisabled = opts.disabledAttrs.includes(attr) if (opts.typeUserDisabledAttrs[type]) { const typeDisabledAttrs = opts.typeUserDisabledAttrs[type] useDefaultAttr.push(!typeDisabledAttrs.includes(attr)) } if (opts.typeUserAttrs[type]) { const userAttrs = Object.keys(opts.typeUserAttrs[type]) useDefaultAttr.push(!userAttrs.includes(attr)) } if (isDisabled && !noDisable.includes(attr)) { useDefaultAttr.push(false) } if (useDefaultAttr.every(Boolean)) { advFields.push(advFieldMap[attr](isDisabled)) } }) // Append custom attributes as defined in typeUserAttrs option if (opts.typeUserAttrs[type]) { const customAttr = processTypeUserAttrs(opts.typeUserAttrs[type], values) advFields.push(customAttr) } return advFields.join('') } /** * 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) { return ( [ ['array', ({ options }) => !!options], [typeof attrData.value, () => true], // string, number, ].find(typeCondition => typeCondition[1](attrData))[0] || 'string' ) } /** * Processes typeUserAttrs * @param {Object} typeUserAttr option * @param {Object} values field attributes * @return {String} markup for custom user attributes */ function processTypeUserAttrs(typeUserAttr, values) { const advField = [] const attrTypeMap = { array: selectUserAttrs, string: inputUserAttrs, number: numberAttribute, boolean: (attr, attrData) => boolAttribute(attr, { ...attrData, [attr]: values[attr] }, { first: attrData.label }), } for (const attribute in typeUserAttr) { if (typeUserAttr.hasOwnProperty(attribute)) { const attrValType = userAttrType(attribute, typeUserAttr[attribute]) const orig = mi18n.get(attribute) const tUA = typeUserAttr[attribute] const origValue = tUA.value || '' tUA.value = values[attribute] || tUA.value || '' if (tUA.label) { i18n[attribute] = tUA.label } if (attrTypeMap[attrValType]) { advField.push(attrTypeMap[attrValType](attribute, tUA)) } i18n[attribute] = orig tUA.value = origValue } } return advField.join('') } /** * Text input value for attribute * @param {String} name * @param {Object} inputAttrs also known as values * @return {String} input markup */ function inputUserAttrs(name, inputAttrs) { const { class: classname, className, ...attrs } = inputAttrs let textAttrs = { id: name + '-' + data.lastID, title: attrs.description || attrs.label || name.toUpperCase(), name: name, type: attrs.type || 'text', className: [`fld-${name}`, (classname || className || '').trim()], } const label = `<label for="${textAttrs.id}">${i18n[name] || ''}</label>` const optionInputs = ['checkbox', 'checkbox-group', 'radio-group'] if (!optionInputs.includes(textAttrs.type)) { textAttrs.className.push('form-control') } textAttrs = Object.assign({}, attrs, textAttrs) const textInput = `<input ${attrString(textAttrs)}>` const inputWrap = `<div class="input-wrap">${textInput}</div>` return `<div class="form-group ${name}-wrap">${label}${inputWrap}</div>` } /** * Select input for multiple choice user attributes * @todo replace with selectAttr * @param {String} name * @param {Object} fieldData * @return {String} select markup */ function selectUserAttrs(name, fieldData) { const { multiple, options, label: labelText, value, class: classname, className, ...restData } = fieldData const optis = Object.keys(options).map(val => { const attrs = { value: val } if (Array.isArray(value) ? value.includes(val) : val === value) { attrs.selected = null } return m('option', options[val], attrs) }) const selectAttrs = { id: `${name}-${data.lastID}`, title: restData.description || labelText || name.toUpperCase(), name, className: `fld-${name} form-control ${classname || className || ''}`.trim(), } if (multiple) { selectAttrs.multiple = true } const label = `<label for="${selectAttrs.id}">${i18n[name]}</label>` Object.keys(restData).forEach(function(attr) { selectAttrs[attr] = restData[attr] }) const select = m('select', optis, selectAttrs).outerHTML const inputWrap = `<div class="input-wrap">${select}</div>` return `<div class="form-group ${name}-wrap">${label}${inputWrap}</div>` } const boolAttribute = (name, values, labels = {}) => { const label = txt => m('label', txt, { for: `${name}-${data.lastID}`, }).outerHTML const cbAttrs = { type: 'checkbox', className: `fld-${name}`, name, id: `${name}-${data.lastID}`, } if (values[name]) { cbAttrs.checked = true } const left = [] let right = [m('input', null, cbAttrs).outerHTML] if (labels.first) { left.push(label(labels.first)) } if (labels.second) { right.push(' ', label(labels.second)) } if (labels.content) { right.push(labels.content) } right = m('div', right, { className: 'input-wrap' }).outerHTML return m('div', left.concat(right), { className: `form-group ${name}-wrap`, }).outerHTML } const btnStyles = style => { let styleField = '' // corrects issue where 'undefined' was saved to formData if (style === 'undefined') { style = 'default' } const styleLabel = `<label>${i18n.style}</label>` styleField += h.input({ value: style || 'default', type: 'hidden', className: 'btn-style', }).outerHTML styleField += '<div class="btn-group" role="group">' styles.btn.forEach(btnStyle => { const classList = ['btn-xs', 'btn', `btn-${btnStyle}`] if (style === btnStyle) { classList.push('selected') } const btn = m('button', mi18n.get(`styles.btn.${btnStyle}`), { value: btnStyle, type: 'button', className: classList.join(' '), }).outerHTML styleField += btn }) styleField += '</div>' styleField = m('div', [styleLabel, styleField], { className: 'form-group style-wrap', }) return styleField.outerHTML } /** * Add a number attribute to a field. * @param {String} attribute * @param {Object} values * @return {String} markup for number attribute */ const numberAttribute = (attribute, values) => { const { class: classname, className, ...attrs } = values const attrVal = attrs[attribute] const attrLabel = mi18n.get(attribute) || attribute const placeholder = mi18n.get(`placeholder.${attribute}`) const inputConfig = { type: 'number', value: attrVal, name: attribute, min: '0', placeholder: placeholder, className: `fld-${attribute} form-control ${classname || className || ''}`.trim(), id: `${attribute}-${data.lastID}`, } const numberAttribute = h.input(trimObj(inputConfig)).outerHTML const inputWrap = `<div class="input-wrap">${numberAttribute}</div>` const inputLabel = `<label for="${inputConfig.id}">${attrLabel}</label>` return m('div', [inputLabel, inputWrap], { className: `form-group ${attribute}-wrap`, }).outerHTML } /** * selectAttribute * @param {String} attribute attribute name * @param {Object} values aka attrs * @param {Array} optionData select field option data * @return {String} select input makrup */ const selectAttribute = (attribute, values, optionData) => { const selectOptions = optionData.map((option, i) => { let optionAttrs = Object.assign( { label: `${i18n.option} ${i}`, value: undefined, }, option ) if (option.value === values[attribute]) { optionAttrs.selected = true } optionAttrs = trimObj(optionAttrs) return m('option', optionAttrs.label, optionAttrs) }) const selectAttrs = { id: attribute + '-' + data.lastID, name: attribute, className: `fld-${attribute} form-control`, } const labelText = mi18n.get(attribute) || capitalize(attribute) || '' const label = m('label', labelText, { for: selectAttrs.id }) const select = m('select', selectOptions, selectAttrs) const inputWrap = m('div', select, { className: 'input-wrap' }) const attrWrap = m('div', [label, inputWrap], { className: `form-group ${selectAttrs.name}-wrap`, }) return attrWrap.outerHTML } /** * Generate some text inputs for field attributes, **will be replaced** * @param {String} attribute * @param {Object} values * @param {Boolean} isHidden * @return {String} */ const textAttribute = (attribute, values, isHidden = false) => { const textArea = ['paragraph'] let attrVal = values[attribute] || '' let attrLabel = mi18n.get(attribute) if (attribute === 'label') { if (textArea.includes(values.type)) { attrLabel = mi18n.get('content') } else { attrVal = parsedHtml(attrVal) } } const placeholder = mi18n.get(`placeholders.${attribute}`) || '' let attributefield = '' const noMakeAttr = [] if (!noMakeAttr.some(elem => elem === true)) { const inputConfig = { name: attribute, placeholder, className: `fld-${attribute} form-control`, id: `${attribute}-${data.lastID}`, } const attributeLabel = m('label', attrLabel, { for: inputConfig.id, }).outerHTML if (attribute === 'label' && !opts.disableHTMLLabels) { inputConfig.contenteditable = true attributefield += m('div', attrVal, inputConfig).outerHTML } else { inputConfig.value = attrVal inputConfig.type = 'text' attributefield += `<input ${attrString(inputConfig)}>` } const inputWrap = `<div class="input-wrap">${attributefield}</div>` let visibility = isHidden ? 'none' : 'block' if (attribute === 'value') { visibility = values.subtype && values.subtype === 'quill' && 'none' } attributefield = m('div', [attributeLabel, inputWrap], { className: `form-group ${attribute}-wrap`, style: `display: ${visibility}`, }) } return attributefield.outerHTML } const requiredField = fieldData => { const { type } = fieldData const noRequire = ['header', 'paragraph', 'button'] const noMake = [] let requireField = '' if (noRequire.includes(type)) { noMake.push(true) } if (!noMake.some(elem => elem === true)) { requireField = boolAttribute('required', fieldData, { first: mi18n.get('required'), }) } return requireField } // Append the new field to the editor const appendNewField = function(values, isNew = true) { const type = values.type || 'text' const label = values.label || (isNew ? i18n.get(type) || mi18n.get('label') : '') const disabledFieldButtons = opts.disabledFieldButtons[type] || values.disabledFieldButtons let fieldButtons = [ m('a', null, { type: 'remove', id: 'del_' + data.lastID, className: 'del-button btn icon-cancel delete-confirm', title: mi18n.get('removeMessage'), }), m('a', null, { type: 'edit', id: data.lastID + '-edit', className: 'toggle-form btn icon-pencil', title: mi18n.get('hide'), }), m('a', null, { type: 'copy', id: data.lastID + '-copy', className: 'copy-button btn icon-copy', title: mi18n.get('copyButtonTooltip'), }), ] if (disabledFieldButtons && Array.isArray(disabledFieldButtons)) { fieldButtons = fieldButtons.filter(btnData => !disabledFieldButtons.includes(btnData.type)) } const liContents = [m('div', fieldButtons, { className: 'field-actions' })] liContents.push( m('label', parsedHtml(label), { className: 'field-label', }) ) liContents.push( m('span', ' *', { className: 'required-asterisk', style: values.required ? 'display:inline' : '', }) ) // add the help icon const descAttrs = { className: 'tooltip-element', tooltip: values.description, style: values.description ? 'display:inline-block' : 'display:none', } liContents.push(m('span', '?', descAttrs)) liContents.push(m('div', '', { className: 'prev-holder' })) const formElements = m('div', [advFields(values), m('a', mi18n.get('close'), { className: 'close-field' })], { className: 'form-elements', }) const editPanel = m('div', formElements, { id: `${data.lastID}-holder`, className: 'frm-holder', dataFieldId: data.lastID, }) formBuilder.currentEditPanel = editPanel liContents.push(editPanel) const field = m('li', liContents, { class: `${type}-field form-field`, type: type, id: data.lastID, }) const $li = $(field) $li.data('fieldData', { attrs: values }) if (typeof h.stopIndex !== 'undefined') { $('> li', d.stage) .eq(h.stopIndex) .before($li) } else { $stage.append($li) } $('.sortable-options', $li).sortable({ update: () => h.updatePreview($li) }) // generate the control, insert it into the list item & add it to the stage h.updatePreview($li) if (opts.typeUserEvents[type] && opts.typeUserEvents[type].onadd) { opts.typeUserEvents[type].onadd(field) } if (isNew) { if (opts.editOnAdd) { h.closeAllEdit() h.toggleEdit(data.lastID, false) } if (field.scrollIntoView && opts.scrollToFieldOnAdd) { field.scrollIntoView({ behavior: 'smooth' }) } } data.lastID = h.incrementId(data.lastID) } // Select field html, since there may be multiple const selectFieldOptions = function(name, optionData, multipleSelect) { 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', } attrs.placeholder = mi18n.get(`placeholder.${prop}`) || '' if (prop === 'selected' && optionData.selected === true) { attrs.checked = optionData.selected } optionInputs.push(m('input', null, attrs)) } } const removeAttrs = { className: 'remove btn icon-cancel', title: mi18n.get('removeMessage'), } optionInputs.push(m('a', null, removeAttrs)) return m('li', optionInputs).outerHTML } const cloneItem = function cloneItem(currentItem) { const currentId = currentItem.attr('id') const type = currentItem.attr('type') const ts = new Date().getTime() const cloneName = type + '-' + ts const $clone = currentItem.clone() $('.fld-name', $clone).val(cloneName) $clone.find('[id]').each((i, elem) => { elem.id = elem.id.replace(currentId, data.lastID) }) $clone.find('[for]').each((index, elem) => { const curId = elem.getAttribute('for') const newForId = curId.replace(currentId, data.lastID) elem.setAttribute('for', newForId) }) $clone.attr('id', data.lastID) $clone.attr('name', cloneName) $clone.addClass('cloned') $('.sortable-options', $clone).sortable() if (opts.typeUserEvents[type] && opts.typeUserEvents[type].onclone) { opts.typeUserEvents[type].onclone($clone[0]) } data.lastID = h.incrementId(data.lastID) return $clone } // ---------------------- Event listeners ---------------------- // const saveAndUpdate = evt => { if (evt) { const isDisabled = [({ type, target }) => type === 'keyup' && target.name === 'className'].some(typeCondition => typeCondition(evt) ) if (isDisabled) { return false } h.updatePreview($(evt.target).closest('.form-field')) h.save.call(h) } } const previewSelectors = ['.form-elements input', '.form-elements select', '.form-elements textarea'].join(', ') // Save field on change $stage.on('change blur keyup click', previewSelectors, throttle(saveAndUpdate, DEFAULT_TIMEOUT, { leading: false })) // delete options $stage.on('click touchstart', '.remove', e => { const $field = $(e.target).parents('.form-field:eq(0)') const field = $field[0] const type = field.getAttribute('type') const $option = $(e.target.parentElement) e.preventDefault() const options = field.querySelector('.sortable-options') const optionsCount = options.childNodes.length if (optionsCount <= 2 && !type.includes('checkbox')) { opts.notify.error('Error: ' + mi18n.get('minOptionMessage')) } else { $option.slideUp('250', () => { $option.remove() h.updatePreview($field) h.save.call(h) }) } }) // touch focus $stage.on('touchstart', 'input', e => { const $input = $(this) if (e.handled !== true) { if ($input.attr('type') === 'checkbox') { $input.trigger('click') } else { $input.focus() const fieldVal = $input.val() $input.val(fieldVal) } } else { return false } }) // toggle fields $stage.on('click touchstart', '.toggle-form, .close-field', function(e) { e.stopPropagation() e.preventDefault() if (e.handled !== true) { const targetID = $(e.target) .parents('.form-field:eq(0)') .attr('id') h.toggleEdit(targetID) e.handled = true } else { return false } }) $stage.on('dblclick', 'li.form-field', e => { if (['select', 'input', 'label'].includes(e.target.tagName.toLowerCase()) || e.target.contentEditable === 'true') { return } e.stopPropagation() e.preventDefault() if (e.handled !== true) { const targetID = e.target.tagName == 'li' ? $(e.target).attr('id') : $(e.target) .closest('li.form-field') .attr('id') h.toggleEdit(targetID) e.handled = true } }) $stage.on('change', '[name="subtype"]', e => { const $field = $(e.target).closest('li.form-field') const $valWrap = $('.value-wrap', $field) $valWrap.toggle(e.target.value !== 'quill') }) const stageOnChangeSelectors = ['.prev-holder input', '.prev-holder select', '.prev-holder textarea'] $stage.on('change', stageOnChangeSelectors.join(', '), e => { let prevOptions if (e.target.classList.contains('other-option')) { return } const field = closest(e.target, '.form-field') const optionTypes = ['select', 'checkbox-group', 'radio-group'] if (optionTypes.includes(field.type)) { const options = field.getElementsByClassName('option-value') if (field.type === 'select') { forEach(options, i => { const selectedOption = options[i].parentElement.childNodes[0] selectedOption.checked = e.target.value === options[i].value }) } else { prevOptions = document.getElementsByName(e.target.name) forEach(prevOptions, i => { const selectedOption = options[i].parentElement.childNodes[0] selectedOption.checked = prevOptions[i].checked }) } } else { const fieldVal = document.getElementById('value-' + field.id) if (fieldVal) { fieldVal.value = e.target.value } } h.save.call(h) }) // update preview to label addEventListeners(d.stage, 'keyup change', ({ target }) => { if (!target.classList.contains('fld-label')) return const value = target.value || target.innerHTML const label = closest(target, '.form-field').querySelector('.field-label') label.innerHTML = parsedHtml(value) }) // remove error styling when users tries to correct mistake $stage.on('keyup', 'input.error', ({ target }) => $(target).removeClass('error')) // update preview for description $stage.on('keyup', 'input[name="description"]', function(e) { const $field = $(e.target).parents('.form-field:eq(0)') const closestToolTip = $('.tooltip-element', $field) const ttVal = $(e.target).val() if (ttVal !== '') { if (!closestToolTip.length) { const tt = `<span class="tooltip-element" tooltip="${ttVal}">?</span>` $('.field-label', $field).after(tt) } else { closestToolTip.attr('tooltip', ttVal).css('display', 'inline-block') } } else { if (closestToolTip.length) { closestToolTip.css('display', 'none') } } }) /** * Toggle multiple select options * @param {Object} e click event * @return {String} newType */ $stage.on('change', '.fld-multiple', e => { const newType = e.target.checked ? 'checkbox' : 'radio' const $options = $('.option-selected', $(e.target).closest('.form-elements')) $options.each(i => ($options[i].type = newType)) return newType }) // format name attribute $stage.on('blur', 'input.fld-name', function(e) { e.target.value = safename(e.target.value) if (e.target.value === '') { $(e.target) .addClass('field-error') .attr('placeholder', mi18n.get('cannotBeEmpty')) } else { $(e.target).removeClass('field-error') } }) $stage.on('blur', 'input.fld-maxlength', e => { e.target.value = forceNumber(e.target.value) }) // Copy field $stage.on('click touchstart', '.icon-copy', function(e) { e.preventDefault() const currentItem = $(e.target) .parent() .parent('li') const $clone = cloneItem(currentItem) $clone.insertAfter(currentItem) h.updatePreview($clone) h.save.call(h) }) // Delete field $stage.on('click touchstart', '.delete-confirm', e => { e.preventDefault() const buttonPosition = e.target.getBoundingClientRect() const bodyRect = document.body.getBoundingClientRect() const coords = { pageX: buttonPosition.left + buttonPosition.width / 2, pageY: buttonPosition.top - bodyRect.top - 12, } const deleteID = $(e.target) .parents('.form-field:eq(0)') .attr('id') const $field = $(document.getElementById(deleteID)) document.addEventListener( 'modalClosed', function() { $field.removeClass('deleting') }, false ) // Check if user is sure they want to remove the field if (opts.fieldRemoveWarn) { const warnH3 = m('h3', mi18n.get('warning')) const warnMessage = m('p', mi18n.get('fieldRemoveWarning')) h.confirm([warnH3, warnMessage], () => h.removeField(deleteID), coords) $field.addClass('deleting') } else { h.removeField(deleteID) } }) // Update button style selection $stage.on('click', '.style-wrap button', e => { const $button = $(e.target) const styleVal = $button.val() const $btnStyle = $button.parent().prev('.btn-style') $btnStyle.val(styleVal) $button.siblings('.btn').removeClass('selected') $button.addClass('selected') h.updatePreview($btnStyle.closest('.form-field')) h.save.call(h) }) // Attach a callback to toggle required asterisk $stage.on('click', '.fld-required', e => { $(e.target) .closest('.form-field') .find('.required-asterisk') .toggle() }) // Attach a callback to toggle roles visibility $stage.on('click', 'input.fld-access', function(e) { const roles = $(e.target) .closest('.form-field') .find('.available-roles') const enableRolesCB = $(e.target) roles.slideToggle(250, function() { if (!enableRolesCB.is(':checked')) { $('input[type=checkbox]', roles).removeAttr('checked') } }) }) // Attach a callback to add new options $stage.on('click', '.add-opt', function(e) { e.preventDefault() const $optionWrap = $(e.target).closest('.field-options') const $multiple = $('[name="multiple"]', $optionWrap) const $firstOption = $('.option-selected:eq(0)', $optionWrap) let isMultiple = false if ($multiple.length) { isMultiple = $multiple.prop('checked') } else { isMultiple = $firstOption.attr('type') === 'checkbox' } const name = $firstOption.attr('name').replace(/-option$/, '') $('.sortable-options', $optionWrap).append(selectFieldOptions(name, false, isMultiple)) }) $stage.on('mouseover mouseout', '.remove, .del-button', e => $(e.target) .closest('li') .toggleClass('delete') ) loadFields() if (opts.disableInjectedStyle) { const styleTags = document.getElementsByClassName('formBuilder-injected-style') forEach(styleTags, i => remove(styleTags[i])) } document.dispatchEvent(events.loaded) // Make actions accessible formBuilder.actions = { getFieldTypes: activeOnly => activeOnly ? subtract(controls.getRegistered(), opts.disableFields) : controls.getRegistered(), clearFields: animate => h.removeAllFields(d.stage, animate), showData: h.showData.bind(h), save: h.save.bind(h), addField: (field, index) => { h.stopIndex = data.formData.length ? index : undefined prepFieldVars(field) }, removeField: h.removeField.bind(h), getData: h.getFormData.bind(h), setData: formData => { h.stopIndex = undefined h.removeAllFields(d.stage, false) loadFields(formData) }, setLang: locale => { mi18n.setCurrent.call(mi18n, locale).then(() => { d.stage.dataset.content = mi18n.get('getStarted') controls.init() d.empty(d.formActions) h.formActionButtons().forEach(button => d.formActions.appendChild(button)) }) }, toggleFieldEdit: fieldId => { const fieldIds = Array.isArray(fieldId) ? fieldId : [fieldId] fieldIds.forEach(fId => { if (!['number', 'string'].includes(typeof fId)) { return } if (typeof fId === 'number') { fId = d.stage.children[fId].id } else if (!/^frmb-/.test(fId)) { fId = d.stage.querySelector(fId).id } h.toggleEdit(fId) }) }, toggleAllFieldEdit: () => { forEach(d.stage.children, index => { h.toggleEdit(d.stage.children[index].id) }) }, closeAllFieldEdit: h.closeAllEdit.bind(h), } // set min-height on stage onRender d.onRender(d.controls, () => { // Ensure style has loaded setTimeout(() => { d.stage.style.minHeight = `${d.controls.clientHeight}px` // If option set, controls will remain in view in editor if (opts.stickyControls.enable) { h.stickyControls($stage) } }, 0) }) return formBuilder } ;(function($) { const methods = { init: (options, elems) => { const { i18n, ...opts } = jQuery.extend({}, defaultOptions, options, true) config.opts = opts const i18nOpts = jQuery.extend({}, defaultI18n, i18n, true) methods.instance = { actions: { getFieldTypes: null, addField: null, clearFields: null, closeAllFieldEdit: null, getData: null, removeField: null, save: null, setData: null, setLang: null, showData: null, toggleAllFieldEdit: null, toggleFieldEdit: null, }, get formData() { return methods.instance.actions.getData && methods.instance.actions.getData('json') }, promise: new Promise(function(resolve, reject) { mi18n .init(i18nOpts) .then(() => { elems.each(i => { const formBuilder = new FormBuilder(opts, elems[i]) $(elems[i]).data('formBuilder', formBuilder) Object.assign(methods, formBuilder.actions) methods.instance.actions = formBuilder.actions }) delete methods.instance.promise resolve(methods.instance) }) .catch(opts.notify.error) }), } return methods.instance }, } jQuery.fn.formBuilder = function(methodOrOptions = {}, ...args) { if (methods[methodOrOptions]) { return methods[methodOrOptions].apply(this, args) } else { const instance = methods.init(methodOrOptions, this) Object.assign(methods, instance) return instance } } })(jQuery)