diff --git a/src/js/config.js b/src/js/config.js index 238a4d6f2..02634ab5f 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -84,6 +84,10 @@ export const defaultOptions = { typeUserAttrs: {}, typeUserDisabledAttrs: {}, typeUserEvents: {}, + defaultGridColumnClass: 'col-md-12', + cancelGridModeDistance: 100, + enableColumnInsertMenu: false, + enableEnhancedBootstrapGrid: false, } export const styles = { diff --git a/src/js/control/textarea.tinymce.js b/src/js/control/textarea.tinymce.js index a2edac96d..8dff65282 100644 --- a/src/js/control/textarea.tinymce.js +++ b/src/js/control/textarea.tinymce.js @@ -10,8 +10,8 @@ import controlTextarea from './textarea' * var renderOpts = { * controlConfig: { * 'textarea.tinymce': { -* paste_data_images: false -* } + * paste_data_images: false + * } * } * }; * ``` @@ -80,13 +80,25 @@ export default class controlTinymce extends controlTextarea { // define options & allow them to be overwritten in the class config const options = jQuery.extend(this.editorOptions, this.classConfig) options.target = this.field - // initialise the editor - window.tinymce.init(options) + + setTimeout(() => { + // initialise the editor + window.tinymce.init(options) + }, 100) // Set userData if (this.config.userData) { window.tinymce.editors[this.id].setContent(this.parsedHtml(this.config.userData[0])) } + + if (window.lastFormBuilderCopiedTinyMCE) { + const timeout = setTimeout(() => { + window.tinymce.editors[this.id].setContent(this.parsedHtml(window.lastFormBuilderCopiedTinyMCE)) + window.lastFormBuilderCopiedTinyMCE = null + clearTimeout(timeout) + }, 300) + } + return evt } } diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 94b710b24..689c0f716 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -29,6 +29,22 @@ import { import { css_prefix_text } from '../fonts/config.json' const DEFAULT_TIMEOUT = 333 +const rowWrapperClassSelector = '.rowWrapper' +const rowWrapperClass = rowWrapperClassSelector.replace('.', '') + +const colWrapperClassSelector = '.colWrapper' +const colWrapperClass = colWrapperClassSelector.replace('.', '') + +const tmpColWrapperClassSelector = '.tempColWrapper' +const tmpColWrapperClass = tmpColWrapperClassSelector.replace('.', '') + +const tmpRowPlaceholderClassSelector = '.tempRowWrapper' +const tmpRowPlaceholderClass = tmpRowPlaceholderClassSelector.replace('.', '') + +const invisibleRowPlaceholderClassSelector = '.invisibleRowPlaceholder' +const invisibleRowPlaceholderClass = invisibleRowPlaceholderClassSelector.replace('.', '') + +let isMoving = false const FormBuilder = function (opts, element, $) { const formBuilder = this @@ -37,6 +53,12 @@ const FormBuilder = function (opts, element, $) { const data = new Data(formID) const d = new Dom(formID) + let formRows = [] + formBuilder.preserveTempContainers = [] + formBuilder.rowWrapperClassSelector = rowWrapperClassSelector + formBuilder.colWrapperClassSelector = colWrapperClassSelector + formBuilder.colWrapperClass = colWrapperClass + // prepare a new layout object with appropriate templates if (!opts.layout) { opts.layout = layout @@ -57,7 +79,21 @@ const FormBuilder = function (opts, element, $) { const $stage = $(d.stage) const $cbUL = $(d.controls) - // Sortable fields + let insertingNewControl = false + let insertTargetIsRow = false + let insertTargetIsColumn = false + + let $targetInsertWrapper + let cloneControls + + function enhancedBootstrapEnabled() { + if (!opts.enableEnhancedBootstrapGrid) { + return false + } + + return true + } + $stage.sortable({ cursor: 'move', opacity: 0.9, @@ -75,33 +111,76 @@ const FormBuilder = function (opts, element, $) { $stage.sortable('disable') } - // ControlBox with different fields - $cbUL.sortable({ - helper: 'clone', - opacity: 0.9, - connectWith: $stage, - cancel: '.formbuilder-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 (!enhancedBootstrapEnabled()) { + $cbUL.sortable({ + helper: 'clone', + opacity: 0.9, + connectWith: $stage, + cancel: '.formbuilder-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 { + if (ui.item.parent()[0] === d.stage) { + h.doCancel = true + processControl(ui.item) + } else { + h.setFieldOrder($cbUL) + h.doCancel = !opts.sortableControls + } + }, + }) + } else { + // ControlBox with different fields + $cbUL.sortable({ + opacity: 0.9, + connectWith: rowWrapperClassSelector, + cancel: '.formbuilder-separator', + cursor: 'move', + scroll: false, + start: (evt, ui) => { + h.startMoving.call(h, evt, ui) + isMoving = true + }, + stop: (evt, ui) => { + h.stopMoving.call(h, evt, ui) + isMoving = false + cleanupTempPlaceholders() + }, + revert: 150, + beforeStop: (evt, ui) => { + h.beforeStop.call(h, evt, ui) + }, + distance: 3, + update: function (event) { + isMoving = false + if (h.doCancel) { + return false + } + + //If started to enter a control into row but then moved it back, hide the placeholders again + if ($(event.target).attr('id') == $cbUL.attr('id')) { + HideInvisibleRowPlaceholders() + } h.setFieldOrder($cbUL) h.doCancel = !opts.sortableControls - } - }, + }, + }) + } + + $cbUL.on('mouseenter', function () { + if (stageHasFields()) { + $stage.children(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + } }) const processControl = control => { @@ -132,6 +211,8 @@ const FormBuilder = function (opts, element, $) { const $editorWrap = $(d.editorWrap) + $('
').appendTo($editorWrap) + const cbWrap = m('div', d.controls, { id: `${data.formID}-cb-wrap`, className: `cb-wrap ${data.layout.controls}`, @@ -141,6 +222,12 @@ const FormBuilder = function (opts, element, $) { cbWrap.appendChild(d.formActions) } + const gridModeHelp = m('div', '', { + id: `${data.formID}-gridModeHelp`, + className: 'grid-mode-help', + }) + cbWrap.appendChild(gridModeHelp) + $editorWrap.append(d.stage, cbWrap) if (element.type !== 'textarea') { @@ -151,6 +238,16 @@ const FormBuilder = function (opts, element, $) { } $(d.controls).on('click', 'li', ({ target }) => { + //Prevent duplicate add when click & dragging control to specific spot + if (isMoving) { + return + } + + //Remove initial placeholder if simply clicking to add field into blank stage + if (!stageHasFields()) { + $stage.find(tmpRowPlaceholderClassSelector).eq(0).remove() + } + const $control = $(target).closest('li') h.stopIndex = undefined processControl($control) @@ -250,9 +347,15 @@ const FormBuilder = function (opts, element, $) { const loadFields = function (formData) { formData = h.getData(formData) if (formData && formData.length) { + formData.forEach(field => { + CaptureRowData(field) + }) + formData.forEach(fieldData => prepFieldVars(trimObj(fieldData))) d.stage.classList.remove('empty') } else if (opts.defaultFields && opts.defaultFields.length) { + config.opts.defaultFields.forEach(field => CaptureRowData(field)) + h.addDefaultFields() } else if (!opts.prepend && !opts.append) { d.stage.classList.add('empty') @@ -266,6 +369,14 @@ const FormBuilder = function (opts, element, $) { h.save() } + //Capture information of all the row- values so generating new values will not ever clash with existing data + function CaptureRowData(field) { + const gridRowValue = h.getRowValue(field.className) + if (gridRowValue && !formRows.includes(gridRowValue)) { + formRows.push(gridRowValue) + } + } + /** * Add data for field with options [select, checkbox-group, radio-group] * @@ -906,6 +1017,7 @@ const FormBuilder = function (opts, element, $) { // Append the new field to the editor const appendNewField = function (values, isNew = true) { + const columnData = prepareFieldRow(values) data.lastID = h.incrementId(data.lastID) const type = values.type || 'text' @@ -935,6 +1047,17 @@ const FormBuilder = function (opts, element, $) { }), ] + if (enhancedBootstrapEnabled()) { + fieldButtons.push( + m('a', null, { + type: 'grid', + id: data.lastID + '-grid', + className: `grid-button btn ${css_prefix_text}grid`, + title: 'Grid Mode', + }), + ) + } + if (disabledFieldButtons && Array.isArray(disabledFieldButtons)) { fieldButtons = fieldButtons.filter(btnData => !disabledFieldButtons.includes(btnData.type)) } @@ -962,7 +1085,9 @@ const FormBuilder = function (opts, element, $) { } liContents.push(m('span', '?', descAttrs)) - liContents.push(m('div', '', { className: 'prev-holder' })) + const prevHolder = m('div', '', { className: 'prev-holder', dataFieldId: data.lastID }) + liContents.push(prevHolder) + const formElements = m('div', [advFields(values), m('a', mi18n.get('close'), { className: 'close-field' })], { className: 'form-elements', }) @@ -984,6 +1109,8 @@ const FormBuilder = function (opts, element, $) { }) const $li = $(field) + AttatchColWrapperHandler($li) + $li.data('fieldData', { attrs: values }) if (typeof h.stopIndex !== 'undefined') { @@ -997,6 +1124,68 @@ const FormBuilder = function (opts, element, $) { // generate the control, insert it into the list item & add it to the stage h.updatePreview($li) + let rowWrapperNode + + if (enhancedBootstrapEnabled()) { + const targetRow = `div.row-${columnData.rowNumber}` + + //Check if an overall row already exists for the field, else create one + if ($stage.children(targetRow).length) { + rowWrapperNode = $stage.children(targetRow) + } else { + rowWrapperNode = m('div', null, { + id: `${field.id}-row`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, + }) + } + + //Turn the placeholder into the new row. Copy some attributes over + if (insertingNewControl && insertTargetIsRow) { + $targetInsertWrapper.attr('id', rowWrapperNode.id) + $targetInsertWrapper.attr('class', rowWrapperNode.className) + $targetInsertWrapper.attr('style', '') + rowWrapperNode = $targetInsertWrapper + } + + //Add a wrapper div for the field itself. This div will be the rendered representation + const colWrapperNode = m('div', null, { + id: `${field.id}-cont`, + className: `${columnData.columnSize} ${colWrapperClass}`, + }) + + if (insertingNewControl && insertTargetIsColumn) { + if ($targetInsertWrapper.attr('prepend') == 'true') { + $(colWrapperNode).prependTo(rowWrapperNode) + } else { + $(colWrapperNode).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) + } + } + + //Control insert will take care of inserting itself + if (!insertTargetIsColumn) { + $(colWrapperNode).appendTo(rowWrapperNode) + } + + //If inserting, use the existing index, do not always append to end + if (!insertingNewControl) { + $stage.append(rowWrapperNode) + } + + $li.appendTo(colWrapperNode) + + setupSortableRowWrapper(rowWrapperNode) + + SetupInvisibleRowPlaceholders(rowWrapperNode) + + //Record the fact that this field did not originally have column information stored. + //If no other fields were added to the same row and the user did not do anything with this information, then remove it when exporting the config + if (columnData.addedDefaultColumnClass) { + $li.attr('addedDefaultColumnClass', true) + } + + h.tmpCleanPrevHolder($(prevHolder)) + } + if (opts.typeUserEvents[type] && opts.typeUserEvents[type].onadd) { opts.typeUserEvents[type].onadd(field) } @@ -1010,6 +1199,312 @@ const FormBuilder = function (opts, element, $) { field.scrollIntoView({ behavior: 'smooth' }) } } + + if (enhancedBootstrapEnabled()) { + //Autosize entire row when using new insert mode + if (insertingNewControl && insertTargetIsColumn) { + autoSizeRowColumns(rowWrapperNode, true) + } + + cleanupTempPlaceholders() + } + + insertingNewControl = false + insertTargetIsRow = false + insertTargetIsColumn = false + } + + function AttatchColWrapperHandler(colWrapper) { + if (!enhancedBootstrapEnabled()) { + return + } + + colWrapper.mouseenter(function (e) { + if (!gridMode) { + HideInvisibleRowPlaceholders() + + //Only show the placeholder for what is above/below the rowWrapper + $(this) + .closest(rowWrapperClassSelector) + .prevAll(tmpRowPlaceholderClassSelector) + .first() + .removeClass(invisibleRowPlaceholderClass) + $(this) + .closest(rowWrapperClassSelector) + .nextAll(tmpRowPlaceholderClassSelector) + .first() + .removeClass(invisibleRowPlaceholderClass) + + gridModeTargetField = $(this) + gridModeStartX = e.pageX + gridModeStartY = e.pageY + } + }) + } + + function HideInvisibleRowPlaceholders() { + $stage.find(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + } + + function SetupInvisibleRowPlaceholders(rowWrapperNode) { + const wrapperClone = $(rowWrapperNode).clone() + wrapperClone.addClass(invisibleRowPlaceholderClass).addClass(tmpRowPlaceholderClass).html('') + wrapperClone.css('height', '1px') + + wrapperClone.attr('class', wrapperClone.attr('class').replace('row-', '')) + wrapperClone.removeAttr('id') + + if ($(rowWrapperNode).index() == 0) { + const wrapperClone2 = $(wrapperClone).clone() + $stage.prepend(wrapperClone2) + setupSortableRowWrapper(wrapperClone2) + } + + wrapperClone.insertAfter($(rowWrapperNode)) + setupSortableRowWrapper(wrapperClone) + } + + function ResetAllInvisibleRowPlaceholders() { + $stage.children(tmpRowPlaceholderClassSelector).remove() + + $stage.children(rowWrapperClassSelector).each((i, elem) => { + SetupInvisibleRowPlaceholders($(elem)) + }) + } + + function setupSortableRowWrapper(rowWrapperNode) { + if (!enhancedBootstrapEnabled()) { + return + } + + $(rowWrapperNode).sortable({ + connectWith: [rowWrapperClassSelector], + cursor: 'move', + opacity: 0.9, + revert: 150, + tolerance: 'pointer', + helper: function (e, el) { + //Shrink the control a little while dragging so it's not in the way as much + const clone = el.clone() + clone.find('.field-actions').remove() + clone.css({ width: '20%', height: '100px', minHeight: '60px', overflow: 'hidden' }) + return clone + }, + over: function (event) { + const overTarget = $(event.target) + const overTargetIsPlaceholder = overTarget.hasClass(tmpRowPlaceholderClass) + + if (!overTargetIsPlaceholder) { + removeColumnInsertButtons(overTarget) + } + + overTarget.addClass('hoverDropStyleInverse') + + if (!overTargetIsPlaceholder) { + HideInvisibleRowPlaceholders() + + //Only show the placeholder for what is above/below the rowWrapper + overTarget + .prevAll(tmpRowPlaceholderClassSelector) + .first() + .removeClass(invisibleRowPlaceholderClass) + .css('height', '40px') + overTarget + .nextAll(tmpRowPlaceholderClassSelector) + .first() + .removeClass(invisibleRowPlaceholderClass) + .css('height', '40px') + } + }, + out: function (event) { + $stage.children(tmpRowPlaceholderClassSelector).removeClass('hoverDropStyleInverse') + $(event.target).removeClass('hoverDropStyleInverse') + }, + placeholder: 'hoverDropStyleInverse', + receive: function (event, ui) { + const senderIsControlsBox = $(ui.sender).attr('id') == $cbUL.attr('id') + + const droppingToNewRow = $(ui.item).parent().hasClass(tmpRowPlaceholderClass) + const droppingToPlaceholderRow = $(ui.item).parent().hasClass(tmpRowPlaceholderClass) + const droppingToExistingRow = + $(ui.item).parent().hasClass(rowWrapperClass) && !$(ui.item).parent().hasClass(tmpRowPlaceholderClass) + + if (droppingToNewRow && !senderIsControlsBox) { + const colWrapper = $(ui.item) + + const columnData = prepareFieldRow({}) + + const rowWrapperNode = m('div', null, { + id: `${colWrapper.find('li').attr('id')}-row`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, + }) + + $(ui.item).parent().replaceWith(rowWrapperNode) + AttatchColWrapperHandler($(ui.item)) + + colWrapper.appendTo(rowWrapperNode) + + setupSortableRowWrapper(rowWrapperNode) + syncFieldWithNewRow(colWrapper.attr('id')) + checkRowCleanup() + } + + if (droppingToPlaceholderRow && senderIsControlsBox) { + insertTargetIsRow = true + insertingNewControl = true + $targetInsertWrapper = $(ui.item).parent() + } + + if (droppingToExistingRow && senderIsControlsBox) { + //Look for the closest add control button and act as if that was used to add the control + if ($(ui.item).prev().hasClass('btnAddControl')) { + $targetInsertWrapper = $(ui.item).prev() + } else if ($(ui.item).next().hasClass('btnAddControl')) { + $targetInsertWrapper = $(ui.item).next() + } else { + $targetInsertWrapper = $(ui.item).attr('prepend', 'true') + } + + const parentRowValue = h.getRowClass($(ui.item).parent().attr('class')) + $targetInsertWrapper.addClass(parentRowValue) + + insertTargetIsColumn = true + insertingNewControl = true + + h.stopIndex = undefined + } + + cleanupTempPlaceholders() + + if (insertingNewControl) { + h.doCancel = true + processControl(ui.item) + h.save.call(h) + } + + ResetAllInvisibleRowPlaceholders() + + const listFieldItem = $(ui.item).find('li') + if (listFieldItem.length) { + CheckTinyMCETransition(listFieldItem) + UpdatePreviewAndSave(listFieldItem) + } + }, + start: function () { + cleanupTempPlaceholders() + }, + stop: function (event, ui) { + $stage.children(tmpRowPlaceholderClassSelector).removeClass('hoverDropStyleInverse') + autoSizeRowColumns(ui.item.closest(rowWrapperClassSelector), true) + }, + update: function (event, ui) { + syncFieldWithNewRow(ui.item.attr('id')) + }, + }) + + $(rowWrapperNode).off('mouseenter') + $(rowWrapperNode).on('mouseenter', function (e) { + setupColumnInserts($(e.currentTarget)) + }) + + $(rowWrapperNode).off('mouseleave') + $(rowWrapperNode).on('mouseleave', function (e) { + removeColumnInsertButtons($(e.currentTarget)) + }) + } + + function CheckTinyMCETransition(fieldListItem) { + const isTinyMCE = fieldListItem.find('textarea[type="tinymce"]') + if (isTinyMCE.length) { + window.lastFormBuilderCopiedTinyMCE = window.tinymce.get(isTinyMCE.attr('id')).save() + } + } + + function UpdatePreviewAndSave(fieldListItem) { + h.updatePreview(fieldListItem) + h.save.call(h) + } + + function cleanupTempPlaceholders() { + $stage.find(colWrapperClassSelector).removeClass('colHoverTempStyle') + $stage.find(tmpColWrapperClassSelector).remove() + } + + function setupColumnInserts(rowWrapper) { + if (!opts.enableColumnInsertMenu) { + return + } + + $(rowWrapper) + .children(colWrapperClassSelector) + .each((i, elem) => { + const colWrapper = $(elem) + colWrapper.addClass('colHoverTempStyle') + + if (colWrapper.index() == 0) { + $( + ``, + ).insertBefore(colWrapper) + } + + $( + ``, + ).insertAfter(colWrapper) + }) + } + + function removeColumnInsertButtons(rowWrapper) { + rowWrapper.find(tmpColWrapperClassSelector).remove() + rowWrapper.find(colWrapperClassSelector).removeClass('colHoverTempStyle') + } + + function prepareFieldRow(data) { + let result = {} + + if (!enhancedBootstrapEnabled()) { + return result + } + + result = h.tryParseColumnInfo(data) + TryCreateNew() + + if (!formRows.includes(result.rowNumber)) { + formRows.push(result.rowNumber) + } + + return result + + function TryCreateNew() { + if (!result.rowNumber) { + //Column information wasn't defined, get new default configuration for one. + let nextRow + if (formRows.length == 0) { + nextRow = 1 + } else { + nextRow = Math.max(...formRows) + 1 + } + + result.rowNumber = nextRow + + //If inserting directly into column, use the correct rowNumber + if (insertingNewControl && insertTargetIsColumn) { + result.rowNumber = h.getRowValue($targetInsertWrapper.attr('class')) + } + + result.columnSize = opts.defaultGridColumnClass + + if (!data.className) { + data.className = '' + } + + data.className += ` row-${result.rowNumber} ${result.columnSize}` + result.addedDefaultColumnClass = true + } + } } // Select field html, since there may be multiple @@ -1060,6 +1555,9 @@ const FormBuilder = function (opts, element, $) { const cloneItem = function cloneItem(currentItem) { data.lastID = h.incrementId(data.lastID) + + CheckTinyMCETransition(currentItem) + const currentId = currentItem.attr('id') const type = currentItem.attr('type') const ts = new Date().getTime() @@ -1076,6 +1574,13 @@ const FormBuilder = function (opts, element, $) { elem.setAttribute('for', newForId) }) + //Copy selects(includes subtype if applicable) + const selects = currentItem.find('select') + selects.each(function (i) { + const select = this + $clone.find('select').eq(i).val($(select).val()) + }) + $clone.attr('id', data.lastID) $clone.attr('name', cloneName) $clone.addClass('cloned') @@ -1099,8 +1604,7 @@ const FormBuilder = function (opts, element, $) { return false } - h.updatePreview($(evt.target).closest('.form-field')) - h.save.call(h) + UpdatePreviewAndSave($(evt.target).closest('.form-field')) } } @@ -1123,8 +1627,7 @@ const FormBuilder = function (opts, element, $) { } else { $option.slideUp('250', () => { $option.remove() - h.updatePreview($field) - h.save.call(h) + UpdatePreviewAndSave($field) }) } }) @@ -1266,16 +1769,104 @@ const FormBuilder = function (opts, element, $) { e.target.value = forceNumber(e.target.value) }) + $stage.on('click touchstart', '.btnAddControl', function (evt) { + const btn = $(evt.currentTarget) + + cloneControls = $cbUL.clone() + + cloneControls.hover( + function () {}, + function () { + cloneControls.remove() + }, + ) + + cloneControls.on('click', 'li', ({ target }) => { + insertTargetIsColumn = true + insertingNewControl = true + $targetInsertWrapper = btn + + const $control = $(target).closest('li') + h.stopIndex = undefined + processControl($control) + h.save.call(h) + + cloneControls.remove() + }) + + $stage.append(cloneControls) + + if (btn.index() == 0) { + cloneControls.css({ + position: 'fixed', + left: btn.offset().left, + top: btn.offset().top - $(window).scrollTop(), + }) + } else { + cloneControls.css({ + position: 'fixed', + left: btn.offset().left - 80, + top: btn.offset().top - $(window).scrollTop(), + }) + } + + //Ensure the bottom of the menu is visible when close to the bottom of page + const bottomOfClone = cloneControls.offset().top + cloneControls.outerHeight() + const bottomOfScreen = $(window).scrollTop() + $(window).innerHeight() + if (bottomOfClone > bottomOfScreen) { + cloneControls.css({ top: parseInt(cloneControls.css('top')) - (bottomOfClone - bottomOfScreen) }) + } + }) + // Copy field $stage.on('click touchstart', `.${css_prefix_text}copy`, function (evt) { evt.preventDefault() const currentItem = $(evt.target).parent().parent('li') const $clone = cloneItem(currentItem) - $clone.insertAfter(currentItem) - h.updatePreview($clone) - h.save.call(h) + prepareCloneWrappers($clone, currentItem) + UpdatePreviewAndSave($clone) + + h.tmpCleanPrevHolder($clone.find('.prev-holder')) + + if (opts.editOnAdd) { + h.closeField(data.lastID, false) + } }) + function prepareCloneWrappers($clone, currentItem) { + if (!enhancedBootstrapEnabled()) { + $clone.insertAfter(currentItem) + return + } + + const inputClassElement = $(`#className-${currentItem.attr('id')}`) + const columnData = prepareFieldRow({}) + + const rowWrapper = m('div', null, { + id: `${$clone.attr('id')}-row`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, + }) + + const colWrapper = m('div', null, { + id: `${$clone.attr('id')}-cont`, + className: `${h.getBootstrapColumnClass(inputClassElement.val())} ${colWrapperClass}`, + }) + $(colWrapper).appendTo(rowWrapper) + + let insertAfterElement + if (currentItem.parent().is('div')) { + insertAfterElement = currentItem.closest(rowWrapperClassSelector) + } else if (currentItem.parent().is('ul')) { + insertAfterElement = currentItem + } + + $(rowWrapper).insertAfter(insertAfterElement) + $clone.appendTo(colWrapper) + + setupSortableRowWrapper(rowWrapper) + syncFieldWithNewRow($clone.attr('id')) + } + // Delete field $stage.on('click touchstart', '.delete-confirm', e => { e.preventDefault() @@ -1309,6 +1900,392 @@ const FormBuilder = function (opts, element, $) { } }) + var gridMode = false + var gridModeTargetField + let gridModeStartX + let gridModeStartY + $stage.on('click touchstart', '.grid-button', e => { + e.preventDefault() + + const ID = $(e.target).parents('.form-field:eq(0)').attr('id') + gridModeTargetField = $(document.getElementById(ID)) + gridModeStartX = e.pageX + gridModeStartY = e.pageY + + toggleGridModeActive() + }) + + //Use mousewheel to work resizing + $stage.bind('mousewheel', function (e) { + if (gridMode) { + e.preventDefault() + + const parentCont = gridModeTargetField.closest('div') + const currentColValue = h.getBootstrapColumnValue(parentCont.attr('class')) + + let nextColSize + if (e.originalEvent.wheelDelta / 120 > 0) { + nextColSize = parseInt(currentColValue) + 1 + } else { + nextColSize = parseInt(currentColValue) - 1 + } + + if (nextColSize > 12) { + h.showToast('Column Size cannot exceed 12') + return + } + + if (nextColSize < 1) { + h.showToast('Column Size cannot be less than 1') + return + } + + //Check overall column value, do not allow the entire row to exceed 12 + const rowWrapper = gridModeTargetField.closest(rowWrapperClassSelector) + + let totalRowValueCount = nextColSize + rowWrapper.children(`div${colWrapperClassSelector}`).each((i, elem) => { + const colWrapper = $(`#${elem.id}`) + const fieldID = colWrapper.find('li').attr('id') + + if (fieldID != gridModeTargetField.attr('id')) { + totalRowValueCount += h.getBootstrapColumnValue($(`#${fieldID}-cont`).attr('class')) + } + }) + + if (totalRowValueCount > 12) { + h.showToast('There is a maximum of 12 columns per row') + return + } + + h.syncBootstrapColumnWrapperAndClassProperty(gridModeTargetField.attr('id'), nextColSize) + gridModeTargetField.attr('manuallyChangedDefaultColumnClass', true) + + buildGridModeCurrentRowInfo() + h.toggleHighlight(gridModeTargetField) + } + }) + + //Use W A S D or Arrow Keys to move the field up/down/left/right across the form + //Use R to auto-size all columns in the row equally + $(document).keydown(e => { + if (gridMode) { + e.preventDefault() + const rowWrapper = gridModeTargetField.closest(rowWrapperClassSelector) + + if (e.keyCode == 87 || e.keyCode == 38) { + moveFieldUp(rowWrapper) + } + + if (e.keyCode == 83 || e.keyCode == 40) { + moveFieldDown(rowWrapper) + } + + if (e.keyCode == 65 || e.keyCode == 37) { + moveFieldLeft() + } + + if (e.keyCode == 68 || e.keyCode == 39) { + moveFieldRight() + } + + if (e.keyCode == 82) { + autoSizeRowColumns(rowWrapper, true) + } + + buildGridModeCurrentRowInfo() + removeColumnInsertButtons(rowWrapper) + } + }) + + function moveFieldUp(rowWrapper) { + const previousRowSibling = rowWrapper.prevAll().not(tmpRowPlaceholderClassSelector).first() + if (previousRowSibling.length) { + $(gridModeTargetField.parent().parent()).swapWith(previousRowSibling) + } else { + return + } + h.toggleHighlight(gridModeTargetField) + } + + function moveFieldDown(rowWrapper) { + const nextRowSibling = rowWrapper.nextAll().not(invisibleRowPlaceholderClassSelector).first() + if (nextRowSibling.length) { + $(gridModeTargetField.parent().parent()).swapWith(nextRowSibling) + } else { + return + } + h.toggleHighlight(gridModeTargetField) + } + + function moveFieldLeft() { + const colSibling = gridModeTargetField.parent().prev() + if (colSibling.length) { + gridModeTargetField.parent().after(colSibling) + } + h.toggleHighlight(gridModeTargetField) + } + + function moveFieldRight() { + const colSibling = gridModeTargetField.parent().next() + if (colSibling.length) { + gridModeTargetField.parent().before(colSibling) + } + h.toggleHighlight(gridModeTargetField) + } + function autoSizeRowColumns(rowWrapper, force = false) { + const childRowCount = rowWrapper.children(`div${colWrapperClassSelector}`).length + const newAutoCalcSizeValue = Math.floor(12 / childRowCount) + + rowWrapper.children(`div${colWrapperClassSelector}`).each((i, elem) => { + const colWrapper = $(`#${elem.id}`) + + //Don't auto-size the field if the user had manually adjusted it during this session + if (!force && colWrapper.find('li').attr('manuallyChangedDefaultColumnClass') == 'true') { + h.showToast(`Preserving column size of field ${i + 1} because you had personally adjusted it`, 4000) + return + } + + h.syncBootstrapColumnWrapperAndClassProperty(elem.id.replace('-cont', ''), newAutoCalcSizeValue) + }) + } + + function syncFieldWithNewRow(fieldID) { + if (fieldID) { + const inputClassElement = $(`#className-${fieldID.replace('-cont', '')}`) + if (inputClassElement.val()) { + const oldRow = h.getRowClass(inputClassElement.val()) + const wrapperRow = h.getRowClass(inputClassElement.closest(rowWrapperClassSelector).attr('class')) + inputClassElement.val(inputClassElement.val().replace(oldRow, wrapperRow)) + checkRowCleanup() + } + } + } + + //When mouse moves away a certain distance, cancel grid mode + $(document).mousemove(e => { + if ( + gridMode && + h.getDistanceBetweenPoints(gridModeStartX, gridModeStartY, e.pageX, e.pageY) > config.opts.cancelGridModeDistance + ) { + toggleGridModeActive(false) + } + }) + + $(document).on('checkRowCleanup', (event, data) => { + checkRowCleanup() + + const rowWrapper = $(`#${data.rowWrapperID}`) + if (rowWrapper.length) { + autoSizeRowColumns(rowWrapper, true) + } + + checkSetupBlankStage() + }) + + $(document).on('fieldOpened', (event, data) => { + const rowWrapper = $(`#${data.rowWrapperID}`) + if (rowWrapper.length) { + removeColumnInsertButtons(rowWrapper) + } + }) + + function checkRowCleanup() { + $stage.find(colWrapperClassSelector).each((i, elem) => { + const $colWrapper = $(elem) + if ($colWrapper.is(':empty') && !formBuilder.preserveTempContainers.includes($colWrapper.attr('id'))) { + $colWrapper.remove() + } + }) + + $stage + .children(rowWrapperClassSelector) + .not(tmpRowPlaceholderClassSelector) + .each((i, elem) => { + if ($(elem).children(colWrapperClassSelector).length == 0) { + const rowValue = h.getRowValue($(elem).attr('class')) + formRows = formRows.filter(x => x != rowValue) + $(elem).remove() + } else { + removeColumnInsertButtons($(elem)) + } + }) + } + + function stageHasFields() { + return $stage.find('li').length > 0 + } + + function checkSetupBlankStage() { + if (stageHasFields() || !enhancedBootstrapEnabled()) { + return + } + + const columnData = prepareFieldRow({}) + + const rowWrapperNode = m('div', null, { + id: `${h.incrementId(data.lastID)}-row`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, + }) + + $stage.append(rowWrapperNode) + setupSortableRowWrapper(rowWrapperNode) + ResetAllInvisibleRowPlaceholders() + + //Create 1 invisible placeholder which will allow the initial drag anywhere in the stage + $stage + .find(tmpRowPlaceholderClassSelector) + .eq(0) + .removeClass(invisibleRowPlaceholderClass) + .css({ height: $stage.css('height'), backgroundColor: 'transparent' }) + } + + function toggleGridModeActive(active = true) { + if (active) { + gridMode = true + h.showToast('Starting Grid Mode - Use the mousewheel to resize.', 1500) + + //Hide controls + $cbUL.css('display', 'none') + $(d.formActions).css('display', 'none') + + //Cleanup temp artifacts + cleanupTempPlaceholders() + + buildGridModeHelp() + h.closeAllEdit() + h.toggleHighlight(gridModeTargetField) + HideInvisibleRowPlaceholders() + } else { + h.showToast('Grid Mode Finished', 1500) + + //If when exiting grid mode and the row columns end up being > 12 (This can happen if the user moved a column up/down and exited), auto-resize it. + const rowWrapper = gridModeTargetField.closest(rowWrapperClassSelector) + let totalRowValueCount = 0 + + rowWrapper.children(`div${colWrapperClassSelector}`).each((i, elem) => { + const colWrapper = $(`#${elem.id}`) + const fieldID = colWrapper.find('li').attr('id') + totalRowValueCount += h.getBootstrapColumnValue($(`#${fieldID}-cont`).attr('class')) + }) + + if (totalRowValueCount > 12) { + autoSizeRowColumns(rowWrapper, true) + } + + gridMode = false + gridModeTargetField = null + + $(gridModeHelp).html('') + + //Show controls + $cbUL.css('display', 'unset') + $(d.formActions).css('display', 'unset') + } + } + + function buildGridModeHelp() { + $(gridModeHelp).html(` +
+

Grid Mode

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ActionResult
MOUSEWHEELAdjust the field column size
W or ↑Move entire row up
S or ↓Move entire row down
A or ←Move field left within the row
D or →Move field right within the row
RResize all fields within the row to be maximally equal
+ +
Current Row Fields
+ + + + + + + + + + + + + + + + +
FieldSize
+ +
+ `) + + buildGridModeCurrentRowInfo() + } + + function buildGridModeCurrentRowInfo() { + $(gridModeHelp).find('.gridHelpCurrentRow tbody').empty() + + const rowWrapper = gridModeTargetField.closest(rowWrapperClassSelector) + + rowWrapper.children(`div${colWrapperClassSelector}`).each((i, elem) => { + const colWrapper = $(`#${elem.id}`) + const fieldID = colWrapper.find('li').attr('id') + const fieldType = $(`#${fieldID}`).attr('type') + + let label = $(`#label-${fieldID}`).html() + if (fieldType == 'hidden' || fieldType == 'paragraph') { + label = $(`#name-${fieldID}`).val() + } + + if (!label) { + label = $(`#${fieldID}`).attr('id') + } + + //Highlight the current field being worked on + let currentFieldClass = '' + if (gridModeTargetField.attr('id') == fieldID) { + currentFieldClass = 'currentGridModeFieldHighlight' + } + + $(gridModeHelp).find('.gridHelpCurrentRow tbody').append(` + + ${label} + + ${h.getBootstrapColumnValue($(`#${fieldID}-cont`).attr('class'))} + + + `) + }) + } + // Update button style selection $stage.on('click', '.style-wrap button', e => { const $button = $(e.target) @@ -1318,8 +2295,7 @@ const FormBuilder = function (opts, element, $) { $btnStyle.val(styleVal) $button.siblings('.btn').removeClass('selected') $button.addClass('selected') - h.updatePreview($btnStyle.closest('.form-field')) - h.save() + UpdatePreviewAndSave($btnStyle.closest('.form-field')) }) // Attach a callback to toggle required asterisk @@ -1441,6 +2417,9 @@ const FormBuilder = function (opts, element, $) { if (opts.stickyControls.enable) { h.stickyControls($stage) } + + checkSetupBlankStage() + clearTimeout(onRenderTimeout) }, 0) }) diff --git a/src/js/form-render.js b/src/js/form-render.js index b02faf503..5c5c0fc8c 100644 --- a/src/js/form-render.js +++ b/src/js/form-render.js @@ -100,7 +100,7 @@ class FormRender { // Check if this rowID is created yet or not. let rowGroupNode = document.getElementById(rowID) if (!rowGroupNode) { - rowGroupNode = utils.markup('div', null, { id: rowID, className: 'row form-inline' }) + rowGroupNode = utils.markup('div', null, { id: rowID, className: 'row' }) renderedFormWrap.appendChild(rowGroupNode) } rowGroupNode.appendChild(field) diff --git a/src/js/helpers.js b/src/js/helpers.js index 480cf9a61..de70b87f3 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -14,6 +14,8 @@ import { unique, xmlAttrString, flattenArray, + bootstrapColumnRegex, + getAllGridRelatedClasses, } from './utils' import events from './events' import { config } from './config' @@ -37,6 +39,7 @@ export default class Helpers { this.layout = layout this.handleKeyDown = this.handleKeyDown.bind(this) this.formBuilder = formBuilder + this.toastTimer = null } /** @@ -193,58 +196,94 @@ export default class Helpers { const _this = this if (form.childNodes.length !== 0) { - // build data object - forEach(form.childNodes, function (index, field) { - const $field = $(field) - - if (!$field.hasClass('disabled-field')) { - let fieldData = _this.getTypes($field) - const $roleInputs = $('.roles-field:checked', field) - const roleVals = $roleInputs.map(index => $roleInputs[index].value).get() - - fieldData = Object.assign({}, fieldData, _this.getAttrVals(field)) - - if (fieldData.subtype) { - if (fieldData.subtype === 'quill') { - const id = `${fieldData.name}-preview` - if (window.fbEditors.quill[id]) { - const instance = window.fbEditors.quill[id].instance - const data = instance.getContents() - fieldData.value = window.JSON.stringify(data.ops) - } - } else if (fieldData.subtype === 'tinymce' && window.tinymce) { - const id = `${fieldData.name}-preview` - if (window.tinymce.editors[id]) { - const editor = window.tinymce.editors[id] - fieldData.value = editor.getContent() + const fields = [] + //Get form-fields as expected(within rowWrapper) + forEach(form.childNodes, function (_index, fieldWrapper) { + const $fieldWrapper = $(fieldWrapper) + + //Go one level deeper than the row container to find the li + $fieldWrapper.find('li.form-field').each(function (i, field) { + fields.push(field) + }) + }) + + //Get form-fields that might still be currently editing and are temporarily outside a rowWrapper + forEach(form.childNodes, function (_index, testElement) { + const $testElement = $(testElement) + if ($testElement.is('li') && $testElement.hasClass('form-field')) { + fields.push(testElement) + } + }) + + if (fields.length) { + fields.forEach(field => { + const $field = $(field) + + if (!$field.hasClass('disabled-field')) { + let fieldData = _this.getTypes($field) + const $roleInputs = $('.roles-field:checked', field) + const roleVals = $roleInputs.map(index => $roleInputs[index].value).get() + + fieldData = Object.assign({}, fieldData, _this.getAttrVals(field)) + + if (fieldData.subtype) { + if (fieldData.subtype === 'quill') { + const id = `${fieldData.name}-preview` + if (window.fbEditors.quill[id]) { + const instance = window.fbEditors.quill[id].instance + const data = instance.getContents() + fieldData.value = window.JSON.stringify(data.ops) + } + } else if (fieldData.subtype === 'tinymce' && window.tinymce) { + const id = `${fieldData.name}-preview` + if (window.tinymce.editors[id]) { + const editor = window.tinymce.editors[id] + fieldData.value = editor.getContent() + } } } - } - if (roleVals.length) { - fieldData.role = roleVals.join(',') - } + if (roleVals.length) { + fieldData.role = roleVals.join(',') + } - fieldData.className = fieldData.className || fieldData.class + fieldData.className = fieldData.className || fieldData.class + + //If no other fields were added to the same row and the user did not do anything with this information, then remove it when exporting the config + if ( + fieldData.className && + $field.attr('addeddefaultcolumnclass') == 'true' && + $field.closest(this.formBuilder.rowWrapperClassSelector).children().length == 1 && + fieldData.className.includes(config.opts.defaultGridColumnClass) + ) { + const classes = getAllGridRelatedClasses(fieldData.className) + + if (classes && classes.length > 0) { + classes.forEach(element => { + fieldData.className = fieldData.className.replace(element, '').trim() + }) + } + } - if (fieldData.className) { - const match = /(?:^|\s)btn-(.*?)(?:\s|$)/g.exec(fieldData.className) - if (match) { - fieldData.style = match[1] + if (fieldData.className) { + const match = /(?:^|\s)btn-(.*?)(?:\s|$)/g.exec(fieldData.className) + if (match) { + fieldData.style = match[1] + } } - } - fieldData = trimObj(fieldData) + fieldData = trimObj(fieldData) - const multipleField = fieldData.type && fieldData.type.match(d.optionFieldsRegEx) + const multipleField = fieldData.type && fieldData.type.match(d.optionFieldsRegEx) - if (multipleField) { - fieldData.values = _this.fieldOptionData($field) - } + if (multipleField) { + fieldData.values = _this.fieldOptionData($field) + } - formData.push(fieldData) - } - }) + formData.push(fieldData) + } + }) + } } return formData @@ -676,7 +715,7 @@ export default class Helpers { removeAllFields(stage) { const i18n = mi18n.current const opts = config.opts - const fields = stage.querySelectorAll('li.form-field') + const fields = stage.querySelectorAll(this.formBuilder.rowWrapperClassSelector) const markEmptyArray = [] if (!fields.length) { @@ -736,14 +775,12 @@ export default class Helpers { */ closeAllEdit() { const _this = this - const fields = $('> li.editing', _this.d.stage) - const toggleBtns = $('.toggle-form', _this.d.stage) - const editPanels = $('.frm-holder', fields) - - toggleBtns.removeClass('open') - fields.removeClass('editing') - $('.prev-holder', fields).show() - editPanels.hide() + + $(_this.d.stage) + .find('li.form-field') + .each((i, elem) => { + this.closeField(elem.id, false) + }) } /** @@ -757,10 +794,37 @@ export default class Helpers { if (!field) { return field } + + if ($(field).hasClass('editing')) { + return this.closeField(fieldId, animate) + } else { + return this.openField(fieldId, animate) + } + } + + closeField(fieldId, animate = true) { + const _this = this + + const field = document.getElementById(fieldId) + if (!field) { + return field + } + const $editPanel = $('.frm-holder', field) const $preview = $('.prev-holder', field) + + let currentlyEditing = false + if ($(field).hasClass('editing')) { + currentlyEditing = true + } + + if (!currentlyEditing) { + return field + } + field.classList.toggle('editing') $('.toggle-form', field).toggleClass('open') + if (animate) { $preview.slideToggle(250) $editPanel.slideToggle(250) @@ -769,14 +833,84 @@ export default class Helpers { $editPanel.toggle() } this.updatePreview($(field)) - if (field.classList.contains('editing')) { - this.formBuilder.currentEditPanel = $editPanel[0] - config.opts.onOpenFieldEdit($editPanel[0]) - document.dispatchEvent(events.fieldEditOpened) + + const liContainer = $(`#${fieldId}`) + const rowContainer = $(`#${fieldId}-cont`) + + //Put the li back in its place + rowContainer.append(liContainer) + + this.removeContainerProtection(rowContainer.attr('id')) + + config.opts.onCloseFieldEdit($editPanel[0]) + document.dispatchEvent(events.fieldEditClosed) + + const prevHolder = liContainer.find('.prev-holder') + const resultsTimeout = setTimeout(() => { + clearTimeout(resultsTimeout) + const cleanResults = _this.tmpCleanPrevHolder(prevHolder) + + cleanResults.forEach(result => { + if (result['columnInfo'].columnSize) { + const currentClassRow = rowContainer.attr('class') + if (currentClassRow != result['columnInfo'].columnSize) { + //Keep the wrapping column div sync'd to the column property from the field + rowContainer.attr('class', `${result['columnInfo'].columnSize} ${this.formBuilder.colWrapperClass}`) + _this.tmpCleanPrevHolder(prevHolder) + } + } + }) + }, 300) + + return field + } + + openField(fieldId, animate = true) { + const field = document.getElementById(fieldId) + if (!field) { + return field + } + + const $editPanel = $('.frm-holder', field) + const $preview = $('.prev-holder', field) + + let currentlyEditing = false + if ($(field).hasClass('editing')) { + currentlyEditing = true + } + + if (currentlyEditing) { + return field + } + + field.classList.toggle('editing') + $('.toggle-form', field).toggleClass('open') + + if (animate) { + $preview.slideToggle(250) + $editPanel.slideToggle(250) } else { - config.opts.onCloseFieldEdit($editPanel[0]) - document.dispatchEvent(events.fieldEditClosed) + $preview.toggle() + $editPanel.toggle() } + this.updatePreview($(field)) + + const liContainer = $(`#${fieldId}`) + const colWrapper = $(`#${fieldId}-cont`) + const rowWrapper = colWrapper.closest(this.formBuilder.rowWrapperClassSelector) + + //Mark the container as something we don't want to cleanup immediately + this.formBuilder.preserveTempContainers.push(colWrapper.attr('id')) + + //Temporarily move the li outside(keeping same relative overall spot in the form) so that the field details show in full width regardless of its column size + liContainer.insertAfter(rowWrapper) + + this.formBuilder.currentEditPanel = $editPanel[0] + config.opts.onOpenFieldEdit($editPanel[0]) + document.dispatchEvent(events.fieldEditOpened) + + $(document).trigger('fieldOpened', [{ rowWrapperID: rowWrapper.attr('id') }]) + return field } @@ -896,6 +1030,7 @@ export default class Helpers { } const $field = $(field) + const fieldRowWrapper = $field.closest(this.formBuilder.rowWrapperClassSelector) if (!field) { config.opts.notify.warning('Field not found') return false @@ -919,6 +1054,13 @@ export default class Helpers { } document.dispatchEvent(events.fieldRemoved) + + this.removeContainerProtection(`${fieldID}-cont`) + + setTimeout(() => { + $(document).trigger('checkRowCleanup', [{ rowWrapperID: fieldRowWrapper.attr('id') }]) + }, 300) + return fieldRemoved } @@ -1121,6 +1263,8 @@ export default class Helpers { * @return {Array|String} formData */ getFormData(type = 'js', formatted = false) { + this.closeAllEdit() + const h = this const data = { js: () => h.prepData(h.d.stage), @@ -1130,4 +1274,172 @@ export default class Helpers { return data[type](formatted) } + + tmpCleanPrevHolder($prevHolder) { + const _this = this + const cleanedFields = [] + + const formGroup = $prevHolder.find('.form-group') + tmpCleanColumnInfo(formGroup) + + formGroup.find('*').each(function (i, field) { + tmpCleanColumnInfo($(field)) + }) + + function tmpCleanColumnInfo($field) { + var classAttr = $field.attr('class') + + if (typeof classAttr !== 'undefined' && classAttr !== false) { + const parseResult = _this.tryParseColumnInfo($field[0]) + + $field.attr('class', $field.attr('class').replace('col-', 'tmp-col-')) + $field.attr('class', $field.attr('class').replace('row', 'tmp-row')) + + const result = {} + result['field'] = $field + result['columnInfo'] = parseResult + cleanedFields.push(result) + } + } + + return cleanedFields + } + + tryParseColumnInfo(data) { + const result = {} + + if (data.className) { + const classes = getAllGridRelatedClasses(data.className) + + if (classes && classes.length > 0) { + classes.forEach(element => { + if (element.startsWith('row-')) { + result['rowNumber'] = parseInt(element.replace('row-', '').trim()) + } else { + result['columnSize'] = element + } + }) + } + } + + return result + } + + //Remove one reference that protected this potentially empty container. There may be other open fields needing the container + removeContainerProtection(containerID) { + var index = this.formBuilder.preserveTempContainers.indexOf(containerID) + if (index !== -1) { + this.formBuilder.preserveTempContainers.splice(index, 1) + } + } + + //Briefly highlight on/off + toggleHighlight(field, ms = 600) { + field.addClass('moveHighlight') + setTimeout(function () { + field.removeClass('moveHighlight') + }, ms) + } + + showToast(msg, timeout = 3000) { + if (this.toastTimer != null) { + window.clearTimeout(this.toastTimer) + this.toastTimer = null + } + + this.toastTimer = setTimeout(function () { + $('.snackbar').removeClass('show') + }, timeout) + + $('.snackbar').addClass('show').html(msg) + } + + getDistanceBetweenPoints(x1, y1, x2, y2) { + const y = x2 - x1 + const x = y2 - y1 + + return Math.floor(Math.sqrt(x * x + y * y)) + } + + //Return full row name (row-1) + getRowClass(className) { + if (!className) { + return + } + + const splitClasses = className.split(' ').filter(x => x.startsWith('row-')) + if (splitClasses && splitClasses.length > 0) { + return splitClasses[0] + } + } + + //Return the row value i.e row-2 would return 2 + getRowValue(className) { + if (!className) { + return 0 + } + + const rowClass = this.getRowClass(className) + if (rowClass) { + return parseInt(rowClass.split('-')[1]) + } + } + + //Example className of 'row row-1' would be changed for 'row row-4' where 4 is the newValue + changeRowClass(className, newValue) { + const rowClass = this.getRowClass(className) + return className.replace(rowClass, `row-${newValue}`) + } + + //Return the column size i.e col-md-6 would return 6 + getBootstrapColumnValue(className) { + if (!className) { + return 0 + } + + const bootstrapClass = this.getBootstrapColumnClass(className) + if (bootstrapClass) { + return parseInt(bootstrapClass.split('-')[2]) + } + } + + //Return the prefix (col-md) + getBootstrapColumnPrefix(className) { + if (!className) { + return 0 + } + + const bootstrapClass = this.getBootstrapColumnClass(className) + if (bootstrapClass) { + return `${bootstrapClass.split('-')[0]}-${bootstrapClass.split('-')[1]}` + } + } + + //Return full class name (col-md-6) + getBootstrapColumnClass(className) { + if (!className) { + return + } + + const splitClasses = className.split(' ').filter(className => bootstrapColumnRegex.test(className)) + if (splitClasses && splitClasses.length > 0) { + return splitClasses[0] + } + } + + //Example className of 'row row-1 col-md-6' would be changed for 'row row-1 col-md-4' where 4 is the newValue + changeBootstrapClass(className, newValue) { + const boostrapClass = this.getBootstrapColumnClass(className) + return className.replace(boostrapClass, `${this.getBootstrapColumnPrefix(className)}-${newValue}`) + } + + syncBootstrapColumnWrapperAndClassProperty(fieldID, newValue) { + const colWrapper = $(`#${fieldID}-cont`) + colWrapper.attr('class', this.changeBootstrapClass(colWrapper.attr('class'), newValue)) + + const inputClassElement = $(`#className-${fieldID}`) + if (inputClassElement.val()) { + inputClassElement.val(this.changeBootstrapClass(inputClassElement.val(), newValue)) + } + } } diff --git a/src/js/layout.js b/src/js/layout.js index 43271f821..976c57842 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -1,23 +1,22 @@ // LAYOUT.JS import utils from './utils' +import { getAllGridRelatedClasses } from './utils' const processClassName = (data, field) => { // wrap the output in a form-group div & return let className = data.id ? `formbuilder-${data.type} form-group field-${data.id}` : '' if (data.className) { - let classes = data.className.split(' ') // Lift any col- and row- type class to the form-group wrapper. The row- class denotes the row group it should go to - classes = classes.filter(className => /^col-(xs|sm|md|lg)-([^\s]+)/.test(className) || className.startsWith('row-')) + const classes = getAllGridRelatedClasses(data.className) if (classes && classes.length > 0) { className += ` ${classes.join(' ')}` } // Now that the col- types were lifted, remove from the actual input field - for (let index = 0; index < classes.length; index++) { - const element = classes[index] - field.classList.remove(element) + if (field.classList) { + field.classList.remove(...classes) } } @@ -50,18 +49,18 @@ export default class layout { } return this.markup('div', [label, field], { - className: processClassName(data, field) + className: processClassName(data, field), }) }, noLabel: (field, label, help, data) => { return this.markup('div', field, { - className: processClassName(data, field) + className: processClassName(data, field), }) }, hidden: field => { // no wrapper any any visible elements return field - } + }, } // merge in any custom templates @@ -150,7 +149,7 @@ export default class layout { // generate a label element return this.markup('label', labelContents, { for: this.data.id, - className: `formbuilder-${this.data.type}-label` + className: `formbuilder-${this.data.type}-label`, }) } @@ -171,7 +170,7 @@ export default class layout { // generate the default help element return this.markup('span', '?', { className: 'tooltip-element', - tooltip: this.data.description + tooltip: this.data.description, }) } diff --git a/src/js/utils.js b/src/js/utils.js index c69928943..e191c286e 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -17,7 +17,7 @@ window.fbEditors = { * @param {Object} attrs {attrName: attrValue} * @return {Object} Object trimmed of null or undefined values */ -export const trimObj = function(attrs, removeFalse = false) { +export const trimObj = function (attrs, removeFalse = false) { const xmlRemove = [null, undefined, ''] if (removeFalse) { xmlRemove.push(false) @@ -40,7 +40,7 @@ export const trimObj = function(attrs, removeFalse = false) { * @param {String} attr * @return {Boolean} */ -export const validAttr = function(attr) { +export const validAttr = function (attr) { const invalid = [ 'values', 'enableOther', @@ -127,7 +127,7 @@ export const safeAttrName = name => { export const hyphenCase = str => { // eslint-disable-next-line no-useless-escape str = str.replace(/[^\w\s\-]/gi, '') - str = str.replace(/([A-Z])/g, function($1) { + str = str.replace(/([A-Z])/g, function ($1) { return '-' + $1.toLowerCase() }) @@ -162,10 +162,10 @@ export const bindEvents = (element, events) => { * @param {Object} field * @return {String} name */ -export const nameAttr = (function() { +export const nameAttr = (function () { let lepoch let counter = 0 - return function(field) { + return function (field) { const epoch = new Date().getTime() if (epoch === lepoch) { ++counter @@ -204,7 +204,7 @@ export const getContentType = content => { * @param {Object} attributes * @return {Object} DOM Element */ -export const markup = function(tag, content = '', attributes = {}) { +export const markup = function (tag, content = '', attributes = {}) { let contentType = getContentType(content) const { events, ...attrs } = attributes const field = document.createElement(tag) @@ -308,7 +308,7 @@ export const parseOptions = options => { export const parseUserData = userData => { const data = [] - if(userData.length){ + if (userData.length) { const values = userData[0].getElementsByTagName('value') for (let i = 0; i < values.length; i++) { @@ -399,7 +399,7 @@ export const escapeAttrs = attrs => { } // forEach that can be used on nodeList -export const forEach = function(array, callback, scope) { +export const forEach = function (array, callback, scope) { for (let i = 0; i < array.length; i++) { callback.call(scope, i, array[i]) // passes back stuff we need } @@ -511,9 +511,7 @@ export const getStyles = (scriptScr, path) => { link.href = (path || '') + src document.head.appendChild(link) } else { - $(``) - .attr('id', id) - .appendTo($(document.head)) + $(``).attr('id', id).appendTo($(document.head)) } // record this is cached @@ -527,7 +525,7 @@ export const getStyles = (scriptScr, path) => { * @return {String} str capitalized string */ export const capitalize = str => { - return str.replace(/\b\w/g, function(m) { + return str.replace(/\b\w/g, function (m) { return m.toUpperCase() }) } @@ -609,7 +607,7 @@ export const forceNumber = str => str.replace(/[^0-9]/g, '') // subtract the contents of 1 array from another export const subtract = (arr, from) => { - return from.filter(function(a) { + return from.filter(function (a) { return !~this.indexOf(a) }, arr) } @@ -640,6 +638,12 @@ export const removeStyle = id => { return elem.parentElement.removeChild(elem) } +export const bootstrapColumnRegex = /^col-(xs|sm|md|lg)-([^\s]+)/ + +export const getAllGridRelatedClasses = className => { + return className.split(' ').filter(x => bootstrapColumnRegex.test(x) || x.startsWith('row-')) +} + /** * * @param {String} str @@ -738,4 +742,19 @@ utils.splitObject = (obj, keys) => { return [kept, rest] } +$.fn.swapWith = function (that) { + var $this = this + var $that = $(that) + + // create temporary placeholder + var $temp = $('
') + + // 3-step swap + $this.before($temp) + $that.before($this) + $temp.before($that).remove() + + return $this +} + export default utils diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 296cb3cc3..70fc1786a 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -36,7 +36,7 @@ overflow: hidden; } - > li { + li.form-field { &:hover { .field-actions { opacity: 1; @@ -54,7 +54,7 @@ } } - li { + li.form-field { position: relative; padding: 6px; clear: both; @@ -432,27 +432,29 @@ margin-left: 2%; background: $input-border-color; margin-bottom: 0; - border-radius: 5px; + border-radius: 2px; list-style: none; padding: 0; > li { - display: flex; cursor: move; margin: 1px; - padding-right: 28px; + + padding: 6px; + background-color: $white; &:nth-child(1) .remove { display: none; } .remove { - position: absolute; + position: relative; opacity: 1; - right: 8px; + float: right; + right: 14px; height: 18px; width: 18px; - top: 14px; + top: 8px; font-size: 12px; padding: 0; color: $error; @@ -461,7 +463,7 @@ } &:hover { - background-color: $error; + background-color: $error !important; text-decoration: none; color: $white; } @@ -730,3 +732,139 @@ font-size: 12px; cursor: default; } + +/* ------------ Toast Message ------------ */ +.snackbar { + visibility: hidden; /* Hidden by default. Visible on click */ + min-width: 250px; /* Set a default minimum width */ + margin-left: -125px; /* Divide value of min-width by 2 */ + background-color: #333; /* Black background color */ + color: #fff; /* White text color */ + text-align: center; /* Centered text */ + border-radius: 2px; /* Rounded borders */ + padding: 16px; /* Padding */ + position: fixed; /* Sit on top of the screen */ + z-index: 1; /* Add a z-index if needed */ + left: 50%; /* Center the snackbar */ + bottom: 30px; /* 30px from the bottom */ +} + +.snackbar.show { + visibility: visible; + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +@-webkit-keyframes fadein { + from { + bottom: 0; + opacity: 0; + } + to { + bottom: 30px; + opacity: 1; + } +} + +@keyframes fadein { + from { + bottom: 0; + opacity: 0; + } + to { + bottom: 30px; + opacity: 1; + } +} + +@-webkit-keyframes fadeout { + from { + bottom: 30px; + opacity: 1; + } + to { + bottom: 0; + opacity: 0; + } +} + +@keyframes fadeout { + from { + bottom: 30px; + opacity: 1; + } + to { + bottom: 0; + opacity: 0; + } +} +/* ------------ END TOOLTIP ------------ */ + +.ui-state-highlight { + border-radius: 3px; + border: 1px dashed #0d99f2; + border-radius: 3px; + background-color: #e5f5f8; + width: 12px; +} + +.moveHighlight { + border: 1px dashed #0d99f2 !important; + background-color: #e5f5f8 !important; +} + +.currentGridModeFieldHighlight { + background-color: #e5f5f8 !important; +} + +.grid-mode-help { + background-color: $white; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.grid-mode-help-row1 { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 1px; +} +.grid-mode-help-row2 { + white-space: nowrap; +} + +.colHoverTempStyle { + padding-left: 7px !important; + padding-right: 7px !important; + flex: 95 1 0% !important; +} + +.rowWrapper { + margin-left: 0px !important; + margin-right: 0px !important; +} + +.btnAddControl { + border: 0; + background-color: unset; +} + +.hoverColumnDropStyle { + border: 1px dashed #0d99f2; + border-radius: 3px; + background-color: #e5f5f8; + width: 20px; + position: fixed; + margin-left: 40px; +} + +.hoverDropStyleInverse { + background-color: #0d99f2; + border: 1px dashed #e5f5f8; +} + +.invisibleRowPlaceholder { + width: 0px !important; + position: fixed !important; + left: -100px !important; +} diff --git a/src/sass/base/_font.scss b/src/sass/base/_font.scss index 77bd0a614..cd4beec6d 100644 --- a/src/sass/base/_font.scss +++ b/src/sass/base/_font.scss @@ -1,6 +1,7 @@ @font-face { font-family: 'formbuilder-icons'; - src: url('data:application/octet-stream;base64,d09GRgABAAAAABu0AA8AAAAAMegAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+IFQTY21hcAAAAdgAAACqAAACbnpHyFBjdnQgAAAChAAAABMAAAAgBtX/BGZwZ20AAAKYAAAFkAAAC3CKkZBZZ2FzcAAACCgAAAAIAAAACAAAABBnbHlmAAAIMAAAEA4AAByklMHRx2hlYWQAABhAAAAAMwAAADYZ1vNNaGhlYQAAGHQAAAAdAAAAJAc8A2VobXR4AAAYlAAAACEAAABMRoz//2xvY2EAABi4AAAAKAAAAChJjFGYbWF4cAAAGOAAAAAgAAAAIAKGDJhuYW1lAAAZAAAAAZkAAAM5OICt5nBvc3QAABqcAAAAmwAAAN59hsARcHJlcAAAGzgAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZN7OOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGA68YPjkyxz0P4shijmIYRpQmBEkBwAMiQy7AHic7ZHLFYJADEXvAOIP5FOCC1e2ws6CXFlr1jSgL5OUYTiXScIMcHKBA9CKp+igfCh4vNUttd9yqf2Ol+qTrgZstGXfvl9l2BRZjaLndx41a3S20xd6juqe9Z4rAyM3JmYWVm3q+cdQ75bVGmktZcCSOvXEjVni1ixxm5Zo6lii+WOJTGCJnGCJW7ZEnrDE/84SuZP5QBZlPsDXOcDXJZBj9i1g/QFjZzHOAAB4nGNgQAMSEMgc9D8LhAESbAPdAHicrVZpd9NGFB15SZyELCULLWphxMRpsEYmbMGACUGyYyBdnK2VoIsUO+m+8Ynf4F/zZNpz6Dd+Wu8bLySQtOdwmpOjd+fN1czbZRJaktgL65GUmy/F1NYmjew8CemGTctRfCg7eyFlisnfBVEQrZbatx2HREQiULWusEQQ+x5ZmmR86FFGy7akV03KLT3pLlvjQb1V334aOsqxO6GkZjN0aD2yJVUYVaJIpj1S0qZlqPorSSu8v8LMV81QwohOImm8GcbQSN4bZ7TKaDW24yiKbLLcKFIkmuFBFHmU1RLn5IoJDMoHzZDyyqcR5cP8iKzYo5xWsEu20/y+L3mndzk/sV9vUbbkQB/Ijuzg7HQlX4RbW2HctJPtKFQRdtd3QmzZ7FT/Zo/ymkYDtysyvdCMYKl8hRArP6HM/iFZLZxP+ZJHo1qykRNB62VO7Es+gdbjiClxzRhZ0N3RCRHU/ZIzDPaYPh788d4plgsTAngcy3pHJZwIEylhczRJ2jByYCVliyqp9a6YOOV1WsRbwn7t2tGXzmjjUHdiPFsPHVs5UcnxaFKnmUyd2knNoykNopR0JnjMrwMoP6JJXm1jNYmVR9M4ZsaERCICLdxLU0EsO7GkKQTNoxm9uRumuXYtWqTJA/Xco/f05la4udNT2g70s0Z/VqdiOtgL0+lp5C/xadrlIkXp+ukZfkziQdYCMpEtNsOUgwdv/Q7Sy9eWHIXXBtju7fMrqH3WRPCkAfsb0B5P1SkJTIWYVYhWQGKta1mWydWsFqnI1HdDmla+rNMEinIcF8e+jHH9XzMzlpgSvt+J07MjLj1z7UsI0xx8m3U9mtepxXIBcWZ5TqdZlu/rNMfyA53mWZ7X6QhLW6ejLD/UaYHlRzodY3lBC5p038GQizDkAg6QMISlA0NYXoIhLBUMYbkIQ1gWYQjLJRjC8mMYwnIZhrC8rGXV1FNJ49qZWAZsQmBijh65zEXlaiq5VEK7aFRqQ54SbpVUFM+qf2WgXjzyhjmwFkiXyJpfMc6Vj0bl+NYVLW8aO1fAsepvH472OfFS1ouFPwX/1dZUJb1izcOTq/Abhp5sJ6o2qXh0TZfPVT26/l9UVFgL9BtIhVgoyrJscGcihI86nYZqoJVDzGzMPLTrdcuan8P9NzFCFlD9+DcUGgvcg05ZSVnt4KzV19uy3DuDcjgTLEkxN/P6VvgiI7PSfpFZyp6PfB5wBYxKZdhqA60VvNknMQ+Z3iTPBHFbUTZI2tjOBIkNHPOAefOdBCZh6qoN5E7hhg34BWFuwXknXKJ6oyyH7kXs8yik/Fun4kT2qGiMwLPZG2Gv70LKb3EMJDT5pX4MVBWhqRg1FdA0Um6oBl/G2bptQsYO9CMqdsOyrOLDxxb3lZJtGYR8pIjVo6Of1l6iTqrcfmYUl++dvgXBIDUxf3vfdHGQyrtayTJHbQNTtxqVU9eaQ+NVh+rmUfW94+wTOWuabronHnpf06rbwcVcLLD2bQ7SUiYX1PVhhQ2iy8WlUOplNEnvuAcYFhjQ71CKjf+r+th8nitVhdFxJN9O1LfR52AM/A/Yf0f1A9D3Y+hyDS7P95oTn2704WyZrqIX66foNzBrrblZugbc0HQD4iFHrY64yg18pwZxeqS5HOkh4GPdFeIBwCaAxeAT3bWM5lMAo/mMOT7A58xh0GQOgy3mMNhmzhrADnMY7DKHwR5zGHzBnHWAL5nDIGQOg4g5DJ4wJwB4yhwGXzGHwdfMYfANc+4DfMscBjFzGCTMYbCv6dYwzC1e0F2gtkFVoANTT1jcw+JQU2XI/o4Xhv29Qcz+wSCm/qjp9pD6Ey8M9WeDmPqLQUz9VdOdIfU3Xhjq7wYx9Q+DmPpMvxjLZQa/jHyXCgeUXWw+5++J9w/bxUC5AAEAAf//AA94nM1ZW3Bbx3nef88VwMEBDnAO7iAuBxcRoEQKV0qkAEiiREqkJJKmJFKyQNY0HVc0TSlJq9ox7TZynKgvrmcqT6dR22EznXGcTOvIE+fB6kynkpt6PHamje126pdOXyq/+KHNS2EB6r+40KwkZ9xkPJPF2QvP7v5nd//b9y+Jk5C757nb3CzhiERsxEHcxEuCJEJMkiZZMkmmyRyZJ8/Q0cnX9On52ks8UHOHSVesICcTSTmxRhKZZCKzGoOIZ8ATWSED0ejAsu7WOEdYDTtWDJeTU/v61OUQ+PrBpD5ziexID9kHeYnskOoCR4nNQkEkUE8pVCRJWUzWd0FmZ5Z6IhnPYi6+m4uSsCMaXgyC3x84RQIBZYL09XkXiNdr9x4JTr5m4ML+uLuwHWufs7KBtQcvLaz2rX5pa6u9/IWW5RmIrm6tS1378he2sFA7f/LkzIzPpyhP/96l3/2dr3/tqxfXzz/+lcdWHl1+5LeWFuvnzp6cPzl/+tTM3MzcQ7PTJ45PHRk/fGjs4IH9tcpwMTeUzfTvSKeSCTMei0b6wiFf0BcM+L0eQ3e7NKfiUBwujSVnXghmIW+YadPIlzHjU8TH9GJOY4PrtllmnWDoKmRB00UzFk8VtUIF8rGiWYwZphHL9QHXD0YsWWRkTAMKKTOmmdidb+dYXAyD7sm3B7NJuVIRLlWrG5UKPo1qu6pWb+Cb9oMN+HEg64/EQ01XyMQWvLwBezbgtD8beLN1pXWFftJ8EYfR11xq6/s4v1JpnVc1Te2NU100wMhXqt2y2ny32k70jxr+TKD1nVA8HqI/Q3o4uxrINt+/8SkjA++rrtZPqxX8udSGquHUiqYSgjrIdHLzgTqZJ2UyQqrkBPka+ZPa1f1JGnYd2Zng3GE6HovQsNsSnguC2++1cxbZbVn0eRRONjSJE3hZqOtOkeMdNo4DwkM9AC5X33wU+vqUCdVKOUJC8yQUsoeOfPXC2vmvPLa8dO7sqYeOTR0aq1b2jY7s3TNcLhULuwf70z2eI8N9XY53kzMuhLM9bqa31XBPzRULKaQjSobuKedzJdg2vtzt83b7yp/JQmkEiqzIecI4o9OSjHzOM765+db162/1Svju669/eP06/HBz88PXX7+liAnJCp3yu+1XH25uuqyyKSmApWz9l4HQnU/CmUx4ooBKWviwlDCTJRgPZ05sbm4mrl+/nths3tpssCJxHYY229Q22exWCvs2N1e3vRpoFhkp+k44U0oWCslSp8wgbyny9h+5/6B/T3TST3K1wfYBuonEURjXAQ4RIJQDuoS8J9wc4Tg0vtieKo6UC7zgY3qhAh7GLuhqRs7jLUsWEJJFpgpx0dCY+KPYc1Mozc33UO6wprlY3vryB633W+9BDgY0tfme6nKpNKdqNNBSeoOwzsRO/3frKrz86BsKE/POOCLjul95oJ94srYaBl6IgIX3g2wJgigbIInCeB8IAbB4QOQOe0Eaw4kU7VbdCgLhLQJfJxYiixa5TkQbjpfO4OnAKQKgkCOKIss9K6LaZZtsYwLmVAR/VouhIcAsYKFhs4htB6S9bqmTudt3fCzDJZpvvsvyxrV6/Vo3c3rlzcqbHRVtVG/Uo8/CG/XoBvwYC9zV/XuMIpeKpIJ+cA6Ejgc8Q2xUoLaVILgsrgtsWzbclk+hgh14q8AvecFKHKLVsUREImmihIrmJJrq1Jb8oBK3rLqXQjqVDQ+1uGTL0rZ9B2tnO8SFtS+B+kLtwLGpWrVcymbiMUN3Omemp+aOzR0+VJ2sTY7sLVXKldxQppgtppKx/nh/MKBHjajT7dzSbmcYlXuLAe1sMA4YHIojCiMaZxTOdFzUPblSIdV9i8qpi/e+vHdoj2t76MXmi738UVssc23ZRFkMoTh2BDVdLqfxRdDlst83pM3iRpX9qvBGINPuy2UCGy7sDLla32wPL84WN3C0oRutF7vzsXi2MwbPzOjKgr5N0juW9yAZR2k4QR4ip8lZskgukW+Q58hl8m1u/+RrFpSPPyOj0og0unIYhsuiOLyyDwgpFUmpjoJUyBQL9fzuwZ18tj8RDfkEymXp4o5DyYOx/eGq38Klx8wDkVqw4rXwAsfXU/G+gEcQDLfmsAs2RbDVc0O7BviM7nKqvGLNKHViJRbZaqmjhkqjslTHNe7dM7K3TvaQ8vCeMmoXgDhLRBEWCIhwDNGSFRf5l79skQO4SJLJ0szsr7tUbqC70oFfaaU2XOmfb19peVhc+w07z9pfdBY4svYbusIFxHp/9cwzc3PT01NTExNjY7Xa6OjwMCXffuFbz1/+5h/8/jPPPfPcsxvfePqpHg68sP7k2hOr53+7gwYZFnz47JmF+bnTc6dPnZx+aPqh2ZmpE1Mnjh+bmJyYPHpkbHwMkWHtYA2x4Wh1FMHC8MgwwgWGFvK53UODu3YO3IsWtyEHp1UiDuoYF/QsbPdfWiElIGw0MEOMWZ32L4+w776cNMx9NG94H1j1oCGCROP+3KYKXUeSxsy9eudqyDRD3CritjtP5TBxq3eu9nLu3OzpmfpkfaY+0FyfnH1lZub7k5N/MzPzh2xgrj4zz7omz3XnNF0dW7bRUuAXnB4PM5BphugnIfN5l93dvEIH7G7Fbd9KEbfbjlbtvjKKhV1UesmvuJtXGYCtVAlxbvktBds68ZEQ+q0E2UEGyBApkGEy2rFbUOx4sMeCYOMUzrZCFJ5Xlv1eaggewVgJ+KhHFD3LcV1z8HZJlewrbhaJyLK6bAG+L0QhzMNiImJyYSIYYaHOfJBdlBZjgABBJueswHH0VBQoVTj0Zk90P6Ss4Zc4hV/tfcqz1v6W4BFXP/uYutb5mqTKq7/C5xZqJ5ls79xpoPHeLpPVyhZ4RXHcObQTJfJBAmn4DJTJtkgi6sDDVNyFpODBkGB3TyTzWtvldd2eG8WKiaiGudzLKFPJbX9DOi90BRX5fjEUb9w5/ZmPa9y4cefnLFboRSOtK702fFC9dOd/kMHcbEcgaR6GKzfevIFz6MUb1Urz3Uo3NdolXDjUYKTwkHq+q4djWOTwkw7v9xOE/jxZUQCsPFhXiayCTZRt7bPlEE70cJqd4TTCcJrVYrHOEqvVskAsVgtavAMdGrD2qxNZqKWiURm5GDWjyIK+oMeNZ64wqGe1IHrkkQKnOQQjW8aDTqJehiG2D/Dg3dr/xbmcFtNuwC9aymen+mY89JkOV+5c/R47Ihb+4Zkx1euqIWdF9fyEoWty9z+52/SD7q3H2do8w+AIxKHOq5TYwWoj1rrioDbE2aJNWJJBlCRxFitROkUQvB6NRoEwcepsJ9CzbQ6rzDaD4XoEIs62hWOWiNm0zg5GwGgHssWuxQMUsX7Q0Ao11+uN+rVgwgxdwwZ9KRQ37zx9jf6suU5fovMslEz5WldCJgsbzRBc8qVak3CpdQUuIXzBQyd3f8Qdo3eRIzrxozV4oebtAyqEPaqE6/FzGO/hHjEQHJ98zYWCkSYCFdZx43Sdnf064kYgy+hLeB5msQL+NOP6ZLCWun8kWb9/4EINgVQs6vM6HRYZlyHqErLTW05jvIZxuxmXQDT0fK4MpbQXzCLoiOPLpVwEPO/kLucnYFER+NY/8XaBh0Gu73Zr6DZ3TD93+5w+4rmsS/nL+dFxKip86595LGEXf/F2a/Bj+NOwce7jhw3jsoedA9fWhVdRF3g0GA5yqHbACvwhWaS8wK8TgQjrEnprZBBXR34DnSWUMsBM4ajFAsTisDC5tDE24iDclwW5mGQIWELth7LX1EwNPmhluVerGxvNC43GtRvf+/TTjQq80Wi0fgp7COks5Dad/5w7tsGtqH47tvy72t/2220WSeAk2KFYZZGn6CeB1HG+qjnUOtGI060560jNZbhddcSsOkbMdaTt8Xs9deS6L+j31bPppBmLhKVgIBCc3fojGDiVSSXi0b6QFAgGjqJjn0O3Po1OfQpd+kTvqmd75P8FfHkvUBjAQMG97V7n3iz8kvx5c9zd24POHc49D7Oi3KtYn7qn/nnPnm5/qp0BNFDZnjaqG9vy9gSXKi2lytgIrKB/Ta+jj+2rBX2aleMpgXFmLcg6Bu70Md2ne1mkXixUIZ0qMIFGqfagdKNzEeODGOzGU4GbNwOFw4FbN/0TOf/Nm/7chP/mLf9E/kwiwOqc/9Yt9u7WzUCnP4+fle6TIx+JkQG6+yeZdIKzyDDewfbHJeAQJHLWNQ2sCrEu20GRibLsANViE9QVFmLaULOpYKHCKtsSh2LlRJ9qUVnEKPC8MEsEgemxgArfweFjW1TtisZZZaTrUNEiIq2VB9KyPZiW9qWskFmw6QdTlVfbZC2qbe3/T9eNdGfuoaus/tqEa7NfiKbFRle/MNEFlmpmwI/ql+3fkUqa8b6wPxZgob5P8/VU0yWEsm6Gc/OaiXU/YC304vA0i9XjKLTevFHOS14zbZgVBmXZvWYH0lYqzX+Np9MH0+nY0J49S3v3vh15bMfFp/rXIqhObRd0FPVlMXcqh4+/9kQNH35p6fzTTz7Z0R/0S29xNvIsOV6bXJgrcKLgRWsrs3uvcRntq0QFkRfY9QZQEZasQFHj5rCiZJ5jajb19FMX1ldXlhbnT544fnTs6559SzbUNyGeiotmPFUuVGi5tA8KaYYTHKCj38t5vLoodQcU2wOKaa1cShUGYRekd4GI00rlHB5CPoemTIyLkmh4GcDokNpFi/f3u7dIcz/wP5J7xG93gisQtbhAMlrf8UhgWOJ9OiiOwOO76z676vL0YR86GcpTWZDjPg8oauDxoSWPqroCIasOkgNeUCVwW3eFVNX36OCST1V1X1zSwW2JhlygnOB5n93lpKJoO/hvisBx/35CEXmnS/UB69GcPC/ZD0bgHxx2FQlanVTgabc7wLoF5fgtim7z+nFFpIpnW4f9WPO/DiAxDDR9HT7Rt5FPWbKrlg3LFHk0joePDFhhuOFJBjH4OcLz7IKSJ1P78u5hUxD8WcDjQefODkjTxSywe1tzq5VK44+dZYldXdK3dTVEVR489lFVn3Y5sZwtH04mlg9WfgA2Vacf9zsTwKliX3ND1XWVvtKcZzUYUiB2cP/0w99Cl9e9Vz3PrWIMxPzqeG2MR3FxOO2cIBKhripUREslyQhN2eKVCSvIsl0+QghzYDhny4dpWwldGBTNsim1s5Fv53y5nfE9vs5zq9ciG5EG5o/eifxwW/taI9KAjzYamDYa3arRYOb77t3u/9sYKh8hz9ccJvBCFkWdWAAkDm24gjZnAFWaE/gVBmBEjrQv+xB0otIL8jyRZWUC4Rl6AkrtFAOtnZ3h3NoXGr9QU4qxlO4ulrymBbcpbIPUXgSi7u59e1pDQ4BcQiPAbuRxhNSFrKhl2Mc4yG0qYnNTtlpluihZJwrJ5mayAKUEXUwWPpywys3OvfiZM4kSFJL4trSPLrLhnQ66wsZ3OqDwozNn2qMZwYkOjQL5XxyYz4kAAHicY2BkYGAAYoWQLZPj+W2+MnAzvwCKMNzOCL8Go////5/F/II5CMjlYGACiQIAb6wN5AB4nGNgZGBgDvqfBSRf/AcC5hcMQBEUIAwAtq0HpgAAAHicY37BwMAMwgugNC4ciWAzrUNinwLS2SD2//8Ae2MRwgAAAAAAAAABygK4AxQDhgSMBuIH6giCCOwJcAmyCpgK1gw4DQwNZA24DlIAAQAAABMAiAAWAAAAAAACAI4AngBzAAABWwtwAAAAAHicjZLPSutAFId/qVXRgqAXXB/uQhQxjRE3rgoFXbkR7FJI08mfkmbKZCrU/V35IL6BOx/Al9BX8dfpcFGKaMIk3/nOmcnMIQD28IYAy+ucY8kBdhktuYVNXHpeo7/23CbfeV5HB9rzBv2D520c49FzB3/wzBWC9hajMV49B/gbnHluYSe49bxGn3puk/95Xsd+8OR5g/7F8zYGwbvnDg5a5309nZsyL6wc9o8kjuJIhnPRVGWdVJLMbKFNIz3JdG1VVekw1ZNMm8lwVlYjZU7KVNfNjcpnVWJW/IoYKNOUupbTMFrJXalamcSq0WIHzX0eW5tJZvRELv23ZWr0WKU2LKydXnS7n/eEPps7xRwGJXIUsBAc0h7xHSNyQzBkhbByWVWiRoKKJsGMMwqXaRj3ODJGNa1iRUUOkfI5cd7wPeSckpkRKwxOyKmb0eCGJme24rrmF/U/VwycaWgWseCUu4l+Me+KpnY2cScZ/e9Bg3vuMaa1XGdxWuNOJ/yRv55b2NdFbkyT0oeuu5b2Al3e3/TpA2udouQAAAB4nG3IWw7CIBBGYX5FbK133YaLmg6jECkQpInu3mjjm+fpy1EzNbVS/zthhjk0FjBYokGLFTqsscEWO+xxwBEnnNWaxpo4DTlIFW2pSsNO+N6n5/aHy62kMXeFrE+T2+LZXao8q3lIEK7Nx1SE9HdmieyDvvogxnlrJRqmyBJMP9aaonFCVkqbqdCtUHYmjkMvRXPKL6Xep1o2rQB4nGPw3sFwIihiIyNjX+QGxp0cDBwMyQUbGVidNjEwMmiBGJu5mBg5ICw+BjCLzWkX0wGgNCeQze60i8EBwmZmcNmowtgRGLHBoSNiI3OKy0Y1EG8XRwMDI4tDR3JIBEhJJBBs5mFi5NHawfi/dQNL70YmBhcADHYj9AAA') format('woff'); + src: url('data:application/octet-stream;base64,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') + format('woff'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -12,52 +13,95 @@ } } */ - - [class^="formbuilder-icon-"]:before, [class*=" formbuilder-icon-"]:before { - font-family: "formbuilder-icons"; + +[class^='formbuilder-icon-']:before, +[class*=' formbuilder-icon-']:before { + font-family: 'formbuilder-icons'; font-style: normal; font-weight: normal; speak: never; - + display: inline-block; text-decoration: inherit; width: 1em; - margin-right: .2em; + margin-right: 0.2em; text-align: center; /* opacity: .8; */ - + /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; text-transform: none; - + /* fix buttons height, for twitter bootstrap */ line-height: 1em; - + /* Animation center compensation - margins should be symmetric */ /* remove if not needed */ - margin-left: .2em; - + margin-left: 0.2em; + /* you can be more comfortable with increased icons size */ /* font-size: 120%; */ - + /* Uncomment for 3D effect */ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } -.formbuilder-icon-autocomplete:before { content: '\e800'; } /* '' */ -.formbuilder-icon-date:before { content: '\e801'; } /* '' */ -.formbuilder-icon-checkbox:before { content: '\e802'; } /* '' */ -.formbuilder-icon-checkbox-group:before { content: '\e803'; } /* '' */ -.formbuilder-icon-radio-group:before { content: '\e804'; } /* '' */ -.formbuilder-icon-rich-text:before { content: '\e805'; } /* '' */ -.formbuilder-icon-select:before { content: '\e806'; } /* '' */ -.formbuilder-icon-textarea:before { content: '\e807'; } /* '' */ -.formbuilder-icon-text:before { content: '\e808'; } /* '' */ -.formbuilder-icon-pencil:before { content: '\e809'; } /* '' */ -.formbuilder-icon-file:before { content: '\e80a'; } /* '' */ -.formbuilder-icon-hidden:before { content: '\e80b'; } /* '' */ -.formbuilder-icon-cancel:before { content: '\e80c'; } /* '' */ -.formbuilder-icon-button:before { content: '\e80d'; } /* '' */ -.formbuilder-icon-header:before { content: '\e80f'; } /* '' */ -.formbuilder-icon-paragraph:before { content: '\e810'; } /* '' */ -.formbuilder-icon-number:before { content: '\e811'; } /* '' */ -.formbuilder-icon-copy:before { content: '\f24d'; } /* '' */ \ No newline at end of file +.formbuilder-icon-autocomplete:before { + content: '\e800'; +} /* '' */ +.formbuilder-icon-date:before { + content: '\e801'; +} /* '' */ +.formbuilder-icon-checkbox:before { + content: '\e802'; +} /* '' */ +.formbuilder-icon-checkbox-group:before { + content: '\e803'; +} /* '' */ +.formbuilder-icon-radio-group:before { + content: '\e804'; +} /* '' */ +.formbuilder-icon-rich-text:before { + content: '\e805'; +} /* '' */ +.formbuilder-icon-select:before { + content: '\e806'; +} /* '' */ +.formbuilder-icon-textarea:before { + content: '\e807'; +} /* '' */ +.formbuilder-icon-text:before { + content: '\e808'; +} /* '' */ +.formbuilder-icon-pencil:before { + content: '\e809'; +} /* '' */ +.formbuilder-icon-file:before { + content: '\e80a'; +} /* '' */ +.formbuilder-icon-hidden:before { + content: '\e80b'; +} /* '' */ +.formbuilder-icon-cancel:before { + content: '\e80c'; +} /* '' */ +.formbuilder-icon-button:before { + content: '\e80d'; +} /* '' */ +.formbuilder-icon-header:before { + content: '\e80f'; +} /* '' */ +.formbuilder-icon-paragraph:before { + content: '\e810'; +} /* '' */ +.formbuilder-icon-number:before { + content: '\e811'; +} /* '' */ +.formbuilder-icon-copy:before { + content: '\f24d'; +} /* '' */ +.formbuilder-icon-grid:before { + content: url("data:image/svg+xml; utf8, "); +} +.formbuilder-icon-plus:before { + content: url("data:image/svg+xml; utf8,"); +}