From c87127789f6ccaf502c79c3579c92b6c34ecb42b Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 13 Jan 2022 12:44:10 -0500 Subject: [PATCH 01/56] Add Enhanced Column Features --- src/js/config.js | 2 + src/js/form-builder.js | 209 ++++++++++++++++++++++++++-- src/js/helpers.js | 285 +++++++++++++++++++++++++++++++++------ src/js/layout.js | 18 +-- src/sass/_stage.scss | 77 ++++++++++- src/sass/base/_font.scss | 3 +- 6 files changed, 529 insertions(+), 65 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index 238a4d6f2..e81e9de7c 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -84,6 +84,8 @@ export const defaultOptions = { typeUserAttrs: {}, typeUserDisabledAttrs: {}, typeUserEvents: {}, + defaultGridColumnClass: 'col-md-12', + cancelResizeModeDistance: 100, } export const styles = { diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 94b710b24..8da433c91 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -36,6 +36,7 @@ const FormBuilder = function (opts, element, $) { const formID = `frmb-${new Date().getTime()}` const data = new Data(formID) const d = new Dom(formID) + let formRows = [] // prepare a new layout object with appropriate templates if (!opts.layout) { @@ -57,19 +58,7 @@ const FormBuilder = function (opts, element, $) { 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', - }) + $('
').appendTo($stage) if (!opts.allowStageSort) { $stage.sortable('disable') @@ -906,6 +895,8 @@ 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' @@ -933,6 +924,12 @@ const FormBuilder = function (opts, element, $) { className: `copy-button btn ${css_prefix_text}copy`, title: mi18n.get('copyButtonTooltip'), }), + m('a', null, { + type: 'resize', + id: data.lastID + '-resize', + className: `resize-button btn ${css_prefix_text}resize`, + title: 'Resize Mode', + }), ] if (disabledFieldButtons && Array.isArray(disabledFieldButtons)) { @@ -962,7 +959,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', }) @@ -997,6 +996,39 @@ const FormBuilder = function (opts, element, $) { // generate the control, insert it into the list item & add it to the stage h.updatePreview($li) + const targetRow = `div.row-${columnData.rowNumber}` + let rowWrapperNode + + //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} rowWrapper`, + }) + } + + //Add a wrapper div for the field itself. This div will be the rendered representation + const rowGroupNode2 = m('div', null, { + id: `${field.id}-cont`, + className: columnData.columnSize, + }) + $(rowGroupNode2).appendTo(rowWrapperNode) + + $stage.append(rowWrapperNode) + $li.appendTo(rowGroupNode2) + + setupSortableWrapper(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) } @@ -1012,6 +1044,75 @@ const FormBuilder = function (opts, element, $) { } } + function setupSortableWrapper(rowWrapperNode) { + $(rowWrapperNode).sortable({ + connectWith: '.rowWrapper', + cursor: 'move', + opacity: 0.9, + revert: 150, + cursorAt: { + left: 5, + top: 5, + }, + placeholder: 'ui-state-highlight', + grid: [1, 1], + stop: function (event, ui) { + const parentRowClass = ui.item.closest('.rowWrapper') + const childRowCount = parentRowClass.children('div').length + const newAutoCalcSizeValue = Math.floor(12 / childRowCount) + + parentRowClass.children('div').each((i, elem) => { + const colWrapper = $(`#${elem.id}`) + + //Don't auto-resize the field if the user had manually adjusted it during this session + if (colWrapper.find('li').attr('manuallyChangedDefaultColumnClass') == 'true') { + return + } + + h.syncBootstrapColumnWrapperAndClassProperty(elem.id.replace('-cont', ''), newAutoCalcSizeValue) + }) + }, + update: function (event, ui) { + const inputClassElement = $(`#className-${ui.item.attr('id').replace('-cont', '')}`) + if (inputClassElement.val()) { + const oldRow = h.getRowClass(inputClassElement.val()) + const wrapperRow = h.getRowClass(ui.item.closest('.rowWrapper').attr('class')) + inputClassElement.val(inputClassElement.val().replace(oldRow, wrapperRow)) + checkRowCleanup() + } + }, + }) + } + + function prepareFieldRow(data) { + let 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 + const nextRow = Math.max(...formRows) + 1 + result.rowNumber = nextRow + 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 const selectFieldOptions = function (optionData, multipleSelect) { const optionTemplate = { selected: false, label: '', value: '' } @@ -1309,6 +1410,86 @@ const FormBuilder = function (opts, element, $) { } }) + var resizeMode = false + var resizeField + let startResizeX + let startResizeY + $stage.on('click touchstart', '.resize-button', e => { + e.preventDefault() + + resizeMode = true + const ID = $(e.target).parents('.form-field:eq(0)').attr('id') + resizeField = $(document.getElementById(ID)) + startResizeX = e.pageX + startResizeY = e.pageY + + h.showToast('Starting Resize Mode - Use the mousewheel to resize.', 1500) + }) + + //Use mousewheel to work the resize mode + $stage.bind('mousewheel', function (e) { + if (resizeMode) { + //During resize mode dont allow normal scrolling + e.preventDefault() + + const parentCont = resizeField.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 + } + + h.syncBootstrapColumnWrapperAndClassProperty(resizeField.attr('id'), nextColSize) + resizeField.attr('manuallyChangedDefaultColumnClass', true) + + removeNextRowPreview() + $(`${nextColSize}`).insertAfter(parentCont.find('.field-actions')) + } + }) + + //When mouse moves away a certain distance, cancel resize mode + $(document).mousemove(e => { + if ( + resizeMode && + h.getDistanceBetweenPoints(startResizeX, startResizeY, e.pageX, e.pageY) > config.opts.cancelResizeModeDistance + ) { + h.showToast('Resize Mode Finished', 1500) + resizeMode = false + removeNextRowPreview() + } + }) + + function removeNextRowPreview() { + resizeField.closest('div').find('.nextRowPreview').remove() + } + + $(document).on('checkRowCleanup', () => { + checkRowCleanup() + }) + + function checkRowCleanup() { + $('.rowWrapper').each((i, elem) => { + if ($(elem).children().length == 0) { + const rowValue = h.getRowValue($(elem).attr('class')) + formRows = formRows.filter(x => x != rowValue) + $(elem).remove() + } + }) + } + // Update button style selection $stage.on('click', '.style-wrap button', e => { const $button = $(e.target) diff --git a/src/js/helpers.js b/src/js/helpers.js index 480cf9a61..26372e10f 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -37,6 +37,7 @@ export default class Helpers { this.layout = layout this.handleKeyDown = this.handleKeyDown.bind(this) this.formBuilder = formBuilder + this.toastTimer = null } /** @@ -194,56 +195,79 @@ export default class Helpers { 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() + 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) { + 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('.rowWrapper').children().length == 1 && + fieldData.className.includes(config.opts.defaultGridColumnClass) + ) { + const classes = fieldData.className + .split(' ') + .filter(className => /^col-(xs|sm|md|lg)-([^\s]+)/.test(className) || className.startsWith('row-')) + + 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) + } + }) }) } @@ -753,6 +777,8 @@ export default class Helpers { * @return {Node|null} field */ toggleEdit(fieldId, animate = true) { + const _this = this + const field = document.getElementById(fieldId) if (!field) { return field @@ -769,13 +795,40 @@ export default class Helpers { $editPanel.toggle() } this.updatePreview($(field)) + + const prevHolder = $(field).find('.prev-holder') + const fieldID = prevHolder.attr('data-field-id') + const liContainer = $(`#${fieldID}`) + const rowContainer = $(`#${fieldID}-cont`) + if (field.classList.contains('editing')) { + //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(rowContainer.closest('.rowWrapper')) + this.formBuilder.currentEditPanel = $editPanel[0] config.opts.onOpenFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditOpened) } else { + //Put the li back in its place + rowContainer.append(liContainer) + config.opts.onCloseFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditClosed) + + setTimeout(() => { + 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.tmpCleanPrevHolder(prevHolder) + } + } + }) + }, 300) } return field } @@ -919,6 +972,10 @@ export default class Helpers { } document.dispatchEvent(events.fieldRemoved) + + //Remove the column wrapper + $field.parent().remove() + $(document).trigger('checkRowCleanup') return fieldRemoved } @@ -1130,4 +1187,152 @@ 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 = data.className + .split(' ') + .filter(className => /^col-(xs|sm|md|lg)-([^\s]+)/.test(className) || className.startsWith('row-')) + + 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 + } + + 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]) + } + } + + //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 => /^col-(xs|sm|md|lg)-([^\s]+)/.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..110f2823c 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -15,9 +15,11 @@ const processClassName = (data, field) => { } // 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) { + for (let index = 0; index < classes.length; index++) { + const element = classes[index] + field.classList.remove(element) + } } } @@ -50,18 +52,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 +152,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 +173,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/sass/_stage.scss b/src/sass/_stage.scss index 296cb3cc3..bc1f0a481 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; @@ -730,3 +730,76 @@ 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; + } +} + +.ui-state-highlight { + border-color: #00ccff; + background-color: #00ccff; + width: 10px; + border-radius: 6px; +} diff --git a/src/sass/base/_font.scss b/src/sass/base/_font.scss index 77bd0a614..40fd5917f 100644 --- a/src/sass/base/_font.scss +++ b/src/sass/base/_font.scss @@ -60,4 +60,5 @@ .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-copy:before { content: '\f24d'; } /* '' */ +.formbuilder-icon-resize:before { content: url("data:image/svg+xml; utf8, "); } \ No newline at end of file From 2b17aca37dc3dc2477a88f5693ffcfb56d8d535e Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 13 Jan 2022 16:54:58 -0500 Subject: [PATCH 02/56] fix when existing data has no rows initially --- src/js/form-builder.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 8da433c91..d80656b82 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1099,7 +1099,13 @@ const FormBuilder = function (opts, element, $) { function TryCreateNew() { if (!result.rowNumber) { //Column information wasn't defined, get new default configuration for one - const nextRow = Math.max(...formRows) + 1 + let nextRow + if (formRows.length == 0) { + nextRow = 1 + } else { + nextRow = Math.max(...formRows) + 1 + } + result.rowNumber = nextRow result.columnSize = opts.defaultGridColumnClass From 44cfb814f8e86b5452c9688ec9dbd04feca9b95f Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 14 Jan 2022 09:16:06 -0500 Subject: [PATCH 03/56] Do not allow resize button while edit panel open --- src/js/helpers.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/js/helpers.js b/src/js/helpers.js index 26372e10f..e349f7231 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -808,12 +808,14 @@ export default class Helpers { this.formBuilder.currentEditPanel = $editPanel[0] config.opts.onOpenFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditOpened) + this.toggleResizeButtonVisible(fieldID, false) } else { //Put the li back in its place rowContainer.append(liContainer) config.opts.onCloseFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditClosed) + this.toggleResizeButtonVisible(fieldID) setTimeout(() => { const cleanResults = _this.tmpCleanPrevHolder(prevHolder) @@ -1240,6 +1242,15 @@ export default class Helpers { return result } + toggleResizeButtonVisible(fieldId, visible = true) { + if (!visible) { + $(`#${fieldId}-resize`).css('display', 'none') + return + } + + $(`#${fieldId}-resize`).css('display', 'unset') + } + showToast(msg, timeout = 3000) { if (this.toastTimer != null) { window.clearTimeout(this.toastTimer) From 4a3c6183d94fe4f7cc4f6f5cc920cd4698d98c67 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 14 Jan 2022 15:44:58 -0500 Subject: [PATCH 04/56] fix remove button positioning --- src/sass/_stage.scss | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index bc1f0a481..af49f3f99 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -437,22 +437,20 @@ padding: 0; > li { - display: flex; cursor: move; margin: 1px; - padding-right: 28px; &:nth-child(1) .remove { display: none; } .remove { - position: absolute; + position: relative; opacity: 1; - right: 8px; + left: 28px; height: 18px; width: 18px; - top: 14px; + top: 0px; font-size: 12px; padding: 0; color: $error; @@ -461,7 +459,7 @@ } &:hover { - background-color: $error; + background-color: $error !important; text-decoration: none; color: $white; } From 6e6c1369a110f5b6f2f5cf792a0fe1bf18702bf3 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 14 Jan 2022 16:59:50 -0500 Subject: [PATCH 05/56] More option css changex --- src/sass/_stage.scss | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index af49f3f99..32d875039 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -432,13 +432,15 @@ margin-left: 2%; background: $input-border-color; margin-bottom: 0; - border-radius: 5px; + border-radius: 2px; list-style: none; padding: 0; > li { cursor: move; margin: 1px; + padding: 6px; + background-color: $white; &:nth-child(1) .remove { display: none; @@ -447,10 +449,11 @@ .remove { position: relative; opacity: 1; - left: 28px; + float: right; + right: 14px; height: 18px; width: 18px; - top: 0px; + top: 8px; font-size: 12px; padding: 0; color: $error; From 41b21ae3e97bf5ed58294b102051bc6abb5cace3 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sun, 16 Jan 2022 00:17:04 -0500 Subject: [PATCH 06/56] Change resize mode to Grid mode. --- src/js/config.js | 2 +- src/js/form-builder.js | 196 +++++++++++++++++++++++++++++---------- src/js/helpers.js | 18 +++- src/sass/_stage.scss | 17 ++++ src/sass/base/_font.scss | 2 +- 5 files changed, 177 insertions(+), 58 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index e81e9de7c..9bf406bf8 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -85,7 +85,7 @@ export const defaultOptions = { typeUserDisabledAttrs: {}, typeUserEvents: {}, defaultGridColumnClass: 'col-md-12', - cancelResizeModeDistance: 100, + cancelGridModeDistance: 100, } export const styles = { diff --git a/src/js/form-builder.js b/src/js/form-builder.js index d80656b82..3924b40be 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -58,8 +58,6 @@ const FormBuilder = function (opts, element, $) { const $stage = $(d.stage) const $cbUL = $(d.controls) - $('
').appendTo($stage) - if (!opts.allowStageSort) { $stage.sortable('disable') } @@ -121,6 +119,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}`, @@ -896,7 +896,6 @@ 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' @@ -925,10 +924,10 @@ const FormBuilder = function (opts, element, $) { title: mi18n.get('copyButtonTooltip'), }), m('a', null, { - type: 'resize', - id: data.lastID + '-resize', - className: `resize-button btn ${css_prefix_text}resize`, - title: 'Resize Mode', + type: 'grid', + id: data.lastID + '-grid', + className: `grid-button btn ${css_prefix_text}grid`, + title: 'Grid Mode', }), ] @@ -1057,29 +1056,10 @@ const FormBuilder = function (opts, element, $) { placeholder: 'ui-state-highlight', grid: [1, 1], stop: function (event, ui) { - const parentRowClass = ui.item.closest('.rowWrapper') - const childRowCount = parentRowClass.children('div').length - const newAutoCalcSizeValue = Math.floor(12 / childRowCount) - - parentRowClass.children('div').each((i, elem) => { - const colWrapper = $(`#${elem.id}`) - - //Don't auto-resize the field if the user had manually adjusted it during this session - if (colWrapper.find('li').attr('manuallyChangedDefaultColumnClass') == 'true') { - return - } - - h.syncBootstrapColumnWrapperAndClassProperty(elem.id.replace('-cont', ''), newAutoCalcSizeValue) - }) + autoSizeRowColumns(ui.item.closest('.rowWrapper')) }, update: function (event, ui) { - const inputClassElement = $(`#className-${ui.item.attr('id').replace('-cont', '')}`) - if (inputClassElement.val()) { - const oldRow = h.getRowClass(inputClassElement.val()) - const wrapperRow = h.getRowClass(ui.item.closest('.rowWrapper').attr('class')) - inputClassElement.val(inputClassElement.val().replace(oldRow, wrapperRow)) - checkRowCleanup() - } + syncFieldWithNewRow(ui.item.attr('id')) }, }) } @@ -1416,29 +1396,28 @@ const FormBuilder = function (opts, element, $) { } }) - var resizeMode = false - var resizeField - let startResizeX - let startResizeY - $stage.on('click touchstart', '.resize-button', e => { + var gridMode = false + var gridModeTargetField + let gridModeStartX + let gridModeStartY + $stage.on('click touchstart', '.grid-button', e => { e.preventDefault() - resizeMode = true + gridMode = true const ID = $(e.target).parents('.form-field:eq(0)').attr('id') - resizeField = $(document.getElementById(ID)) - startResizeX = e.pageX - startResizeY = e.pageY + gridModeTargetField = $(document.getElementById(ID)) + gridModeStartX = e.pageX + gridModeStartY = e.pageY - h.showToast('Starting Resize Mode - Use the mousewheel to resize.', 1500) + h.showToast('Starting Grid Mode - Use the mousewheel to resize.', 1500) }) - //Use mousewheel to work the resize mode + //Use mousewheel to work resizing $stage.bind('mousewheel', function (e) { - if (resizeMode) { - //During resize mode dont allow normal scrolling + if (gridMode) { e.preventDefault() - const parentCont = resizeField.closest('div') + const parentCont = gridModeTargetField.closest('div') const currentColValue = h.getBootstrapColumnValue(parentCont.attr('class')) let nextColSize @@ -1458,28 +1437,143 @@ const FormBuilder = function (opts, element, $) { return } - h.syncBootstrapColumnWrapperAndClassProperty(resizeField.attr('id'), nextColSize) - resizeField.attr('manuallyChangedDefaultColumnClass', true) + h.syncBootstrapColumnWrapperAndClassProperty(gridModeTargetField.attr('id'), nextColSize) + gridModeTargetField.attr('manuallyChangedDefaultColumnClass', true) removeNextRowPreview() $(`${nextColSize}`).insertAfter(parentCont.find('.field-actions')) } }) - //When mouse moves away a certain distance, cancel resize mode + //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('.rowWrapper') + + 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) + } + } + }) + + function moveFieldUp(rowWrapper) { + const rowSibling = rowWrapper.prev() + if (rowSibling.length) { + gridModeTargetField.parent().appendTo(rowSibling) + syncFieldWithNewRow(gridModeTargetField.attr('id')) + } else { + createNewRow(true) + } + h.toggleHighlight(gridModeTargetField) + } + + function moveFieldDown(rowWrapper) { + const rowSibling = rowWrapper.next() + if (rowSibling.length) { + gridModeTargetField.parent().appendTo(rowSibling) + syncFieldWithNewRow(gridModeTargetField.attr('id')) + } else { + createNewRow() + } + 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 createNewRow(prepend = false) { + const columnData = prepareFieldRow({}) + + const rowWrapperNode = m('div', null, { + id: `${gridModeTargetField.attr('id')}-row`, + className: `row row-${columnData.rowNumber} rowWrapper`, + }) + + gridModeTargetField.parent().appendTo(rowWrapperNode) + + if (prepend) { + $stage.prepend(rowWrapperNode) + } else { + $stage.append(rowWrapperNode) + } + + setupSortableWrapper(rowWrapperNode) + syncFieldWithNewRow(gridModeTargetField.attr('id')) + checkRowCleanup() + } + + function autoSizeRowColumns(rowWrapper, force = false) { + const childRowCount = rowWrapper.children('div').length + const newAutoCalcSizeValue = Math.floor(12 / childRowCount) + + rowWrapper.children('div').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) { + const inputClassElement = $(`#className-${fieldID.replace('-cont', '')}`) + if (inputClassElement.val()) { + const oldRow = h.getRowClass(inputClassElement.val()) + const wrapperRow = h.getRowClass(inputClassElement.closest('.rowWrapper').attr('class')) + inputClassElement.val(inputClassElement.val().replace(oldRow, wrapperRow)) + checkRowCleanup() + } + } + + //When mouse moves away a certain distance, cancel grid mode $(document).mousemove(e => { if ( - resizeMode && - h.getDistanceBetweenPoints(startResizeX, startResizeY, e.pageX, e.pageY) > config.opts.cancelResizeModeDistance + gridMode && + h.getDistanceBetweenPoints(gridModeStartX, gridModeStartY, e.pageX, e.pageY) > config.opts.cancelGridModeDistance ) { - h.showToast('Resize Mode Finished', 1500) - resizeMode = false + h.showToast('Grid Mode Finished', 1500) + gridMode = false removeNextRowPreview() } }) function removeNextRowPreview() { - resizeField.closest('div').find('.nextRowPreview').remove() + gridModeTargetField.closest('div').find('.nextRowPreview').remove() } $(document).on('checkRowCleanup', () => { @@ -1487,7 +1581,7 @@ const FormBuilder = function (opts, element, $) { }) function checkRowCleanup() { - $('.rowWrapper').each((i, elem) => { + $stage.children('.rowWrapper').each((i, elem) => { if ($(elem).children().length == 0) { const rowValue = h.getRowValue($(elem).attr('class')) formRows = formRows.filter(x => x != rowValue) diff --git a/src/js/helpers.js b/src/js/helpers.js index e349f7231..ec42bb7cd 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -808,14 +808,14 @@ export default class Helpers { this.formBuilder.currentEditPanel = $editPanel[0] config.opts.onOpenFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditOpened) - this.toggleResizeButtonVisible(fieldID, false) + this.toggleGridModeButtonVisible(fieldID, false) } else { //Put the li back in its place rowContainer.append(liContainer) config.opts.onCloseFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditClosed) - this.toggleResizeButtonVisible(fieldID) + this.toggleGridModeButtonVisible(fieldID) setTimeout(() => { const cleanResults = _this.tmpCleanPrevHolder(prevHolder) @@ -1242,13 +1242,21 @@ export default class Helpers { return result } - toggleResizeButtonVisible(fieldId, visible = true) { + toggleGridModeButtonVisible(fieldId, visible = true) { if (!visible) { - $(`#${fieldId}-resize`).css('display', 'none') + $(`#${fieldId}-grid`).css('display', 'none') return } - $(`#${fieldId}-resize`).css('display', 'unset') + $(`#${fieldId}-grid`).css('display', 'unset') + } + + //Briefly highlight on/off + toggleHighlight(field, ms = 600) { + field.addClass('moveHighlight') + setTimeout(function () { + field.removeClass('moveHighlight') + }, ms) } showToast(msg, timeout = 3000) { diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 32d875039..837b3a1e3 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -54,6 +54,17 @@ } } + // .rowWrapper { + // //position: relative; + // margin-right: 0px; + // width: 100%; + + // &:hover { + // background-color: black; + // //background-image: linear-gradient(to top, #cfd9df 0%, #f0e68c 100%); + // } + // } + li.form-field { position: relative; padding: 6px; @@ -439,6 +450,7 @@ > li { cursor: move; margin: 1px; + padding: 6px; background-color: $white; @@ -797,6 +809,7 @@ opacity: 0; } } +/* ------------ END TOOLTIP ------------ */ .ui-state-highlight { border-color: #00ccff; @@ -804,3 +817,7 @@ width: 10px; border-radius: 6px; } + +.moveHighlight { + background-color: rgba(171, 234, 250, 0.664) !important; +} diff --git a/src/sass/base/_font.scss b/src/sass/base/_font.scss index 40fd5917f..c237f4244 100644 --- a/src/sass/base/_font.scss +++ b/src/sass/base/_font.scss @@ -61,4 +61,4 @@ .formbuilder-icon-paragraph:before { content: '\e810'; } /* '' */ .formbuilder-icon-number:before { content: '\e811'; } /* '' */ .formbuilder-icon-copy:before { content: '\f24d'; } /* '' */ -.formbuilder-icon-resize:before { content: url("data:image/svg+xml; utf8, "); } \ No newline at end of file +.formbuilder-icon-grid:before { content: url("data:image/svg+xml; utf8, "); } \ No newline at end of file From b1a97ac564e3eec2af1feefd6dda4c3449671238 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sun, 16 Jan 2022 00:26:16 -0500 Subject: [PATCH 07/56] cleanup --- src/sass/_stage.scss | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 837b3a1e3..7b2e3235a 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -54,17 +54,6 @@ } } - // .rowWrapper { - // //position: relative; - // margin-right: 0px; - // width: 100%; - - // &:hover { - // background-color: black; - // //background-image: linear-gradient(to top, #cfd9df 0%, #f0e68c 100%); - // } - // } - li.form-field { position: relative; padding: 6px; From 58444ec2618eaf553a793b922e74fbf2c32f07ef Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sun, 16 Jan 2022 20:56:22 -0500 Subject: [PATCH 08/56] Add Grid Mode UI --- src/js/form-builder.js | 135 +++++++++++++++++++++++++++++++++++++---- src/sass/_stage.scss | 20 ++++++ 2 files changed, 144 insertions(+), 11 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 3924b40be..040b49eea 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -130,6 +130,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') { @@ -1403,13 +1409,12 @@ const FormBuilder = function (opts, element, $) { $stage.on('click touchstart', '.grid-button', e => { e.preventDefault() - gridMode = true const ID = $(e.target).parents('.form-field:eq(0)').attr('id') gridModeTargetField = $(document.getElementById(ID)) gridModeStartX = e.pageX gridModeStartY = e.pageY - h.showToast('Starting Grid Mode - Use the mousewheel to resize.', 1500) + toggleGridModeActive() }) //Use mousewheel to work resizing @@ -1440,8 +1445,7 @@ const FormBuilder = function (opts, element, $) { h.syncBootstrapColumnWrapperAndClassProperty(gridModeTargetField.attr('id'), nextColSize) gridModeTargetField.attr('manuallyChangedDefaultColumnClass', true) - removeNextRowPreview() - $(`${nextColSize}`).insertAfter(parentCont.find('.field-actions')) + buildGridModeCurrentRowInfo() } }) @@ -1471,6 +1475,8 @@ const FormBuilder = function (opts, element, $) { if (e.keyCode == 82) { autoSizeRowColumns(rowWrapper, true) } + + buildGridModeCurrentRowInfo() } }) @@ -1566,16 +1572,10 @@ const FormBuilder = function (opts, element, $) { gridMode && h.getDistanceBetweenPoints(gridModeStartX, gridModeStartY, e.pageX, e.pageY) > config.opts.cancelGridModeDistance ) { - h.showToast('Grid Mode Finished', 1500) - gridMode = false - removeNextRowPreview() + toggleGridModeActive(false) } }) - function removeNextRowPreview() { - gridModeTargetField.closest('div').find('.nextRowPreview').remove() - } - $(document).on('checkRowCleanup', () => { checkRowCleanup() }) @@ -1590,6 +1590,119 @@ const FormBuilder = function (opts, element, $) { }) } + 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') + + buildGridModeHelp() + } else { + h.showToast('Grid Mode Finished', 1500) + gridMode = false + $(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 the field up/into another row
S or ↓Move the field down/into another row
A or ←Move the field left within the row
D or →Move the 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('.rowWrapper') + + rowWrapper.children('div').each((i, elem) => { + const colWrapper = $(`#${elem.id}`) + const fieldID = colWrapper.find('li').attr('id') + const label = $(`#label-${fieldID}`).html() + + //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) diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 7b2e3235a..0fe55814c 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -810,3 +810,23 @@ .moveHighlight { background-color: rgba(171, 234, 250, 0.664) !important; } + +.currentGridModeFieldHighlight { + background-color: rgba(171, 234, 250, 0.664) !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; +} From 2bce84476d653dcb2d62a270a817e5efdc142a2e Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sun, 16 Jan 2022 21:09:31 -0500 Subject: [PATCH 09/56] Fix row cleanup when editing --- src/js/helpers.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index ec42bb7cd..88baa02b6 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -975,8 +975,6 @@ export default class Helpers { document.dispatchEvent(events.fieldRemoved) - //Remove the column wrapper - $field.parent().remove() $(document).trigger('checkRowCleanup') return fieldRemoved } From 8d61376d08cc089b3714ccb9948a45a888119646 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 14:22:03 -0500 Subject: [PATCH 10/56] Fix close all, refactor toggleEdit --- src/js/form-builder.js | 1 + src/js/helpers.js | 133 ++++++++++++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 36 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 040b49eea..1531ad557 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1600,6 +1600,7 @@ const FormBuilder = function (opts, element, $) { $(d.formActions).css('display', 'none') buildGridModeHelp() + h.closeAllEdit() } else { h.showToast('Grid Mode Finished', 1500) gridMode = false diff --git a/src/js/helpers.js b/src/js/helpers.js index 88baa02b6..dbe3b26ad 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -760,14 +760,13 @@ 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) => { + console.log(elem.id) + this.closeField(elem.id, false) + }) } /** @@ -777,16 +776,41 @@ export default class Helpers { * @return {Node|null} field */ toggleEdit(fieldId, animate = true) { + const field = document.getElementById(fieldId) + 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) @@ -801,37 +825,74 @@ export default class Helpers { const liContainer = $(`#${fieldID}`) const rowContainer = $(`#${fieldID}-cont`) - if (field.classList.contains('editing')) { - //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(rowContainer.closest('.rowWrapper')) + //Put the li back in its place + rowContainer.append(liContainer) - this.formBuilder.currentEditPanel = $editPanel[0] - config.opts.onOpenFieldEdit($editPanel[0]) - document.dispatchEvent(events.fieldEditOpened) - this.toggleGridModeButtonVisible(fieldID, false) - } else { - //Put the li back in its place - rowContainer.append(liContainer) - - config.opts.onCloseFieldEdit($editPanel[0]) - document.dispatchEvent(events.fieldEditClosed) - this.toggleGridModeButtonVisible(fieldID) - - setTimeout(() => { - 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.tmpCleanPrevHolder(prevHolder) - } + config.opts.onCloseFieldEdit($editPanel[0]) + document.dispatchEvent(events.fieldEditClosed) + this.toggleGridModeButtonVisible(fieldID) + + setTimeout(() => { + 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.tmpCleanPrevHolder(prevHolder) } - }) - }, 300) + } + }) + }, 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 { + $preview.toggle() + $editPanel.toggle() } + this.updatePreview($(field)) + + const prevHolder = $(field).find('.prev-holder') + const fieldID = prevHolder.attr('data-field-id') + const liContainer = $(`#${fieldID}`) + const rowContainer = $(`#${fieldID}-cont`) + + //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(rowContainer.closest('.rowWrapper')) + + this.formBuilder.currentEditPanel = $editPanel[0] + config.opts.onOpenFieldEdit($editPanel[0]) + document.dispatchEvent(events.fieldEditOpened) + this.toggleGridModeButtonVisible(fieldID, false) + return field } From adbd746abbacc6cd0eb0e6df70ef66bee4262ca4 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 14:26:37 -0500 Subject: [PATCH 11/56] Fix toggle css --- src/js/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index dbe3b26ad..63d15c60d 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -1307,7 +1307,7 @@ export default class Helpers { return } - $(`#${fieldId}-grid`).css('display', 'unset') + $(`#${fieldId}-grid`).css('display', '') } //Briefly highlight on/off From 22fdb090b21bbc4473fb62b743ba14c9f3bc4b54 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 15:13:52 -0500 Subject: [PATCH 12/56] Add entering grid mode with keyboard shortcut E --- src/js/form-builder.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 1531ad557..eff2a4bfb 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -988,6 +988,16 @@ const FormBuilder = function (opts, element, $) { }) const $li = $(field) + $li + .mouseenter(function (e) { + if (!gridMode) { + gridModeTargetField = $(this) + gridModeStartX = e.pageX + gridModeStartY = e.pageY + } + }) + .mouseleave(function () {}) + $li.data('fieldData', { attrs: values }) if (typeof h.stopIndex !== 'undefined') { @@ -1417,6 +1427,14 @@ const FormBuilder = function (opts, element, $) { toggleGridModeActive() }) + //Use E to enter into Grid Mode for the currently active(hovered field) + $(document).keydown(e => { + if (e.keyCode == 69 && gridModeTargetField) { + e.preventDefault() + toggleGridModeActive() + } + }) + //Use mousewheel to work resizing $stage.bind('mousewheel', function (e) { if (gridMode) { @@ -1604,6 +1622,8 @@ const FormBuilder = function (opts, element, $) { } else { h.showToast('Grid Mode Finished', 1500) gridMode = false + gridModeTargetField = null + $(gridModeHelp).html('') //Show controls @@ -1649,6 +1669,10 @@ const FormBuilder = function (opts, element, $) { R Resize all fields within the row to be maximally equal + + E + Enter Grid Mode when hovering over a form field + From 870b7aa8c27c1060217a7ee9af1fd7a15d13a4f4 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 17:25:38 -0500 Subject: [PATCH 13/56] Ensure form-fields that are currently open are saved --- src/js/helpers.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index 63d15c60d..a98d04f9e 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -194,12 +194,28 @@ export default class Helpers { const _this = this if (form.childNodes.length !== 0) { - // build data object + 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')) { + console.log('adding open field ' + $testElement.attr('id')) + fields.push(testElement) + } + }) + + if (fields.length) { + fields.forEach(field => { const $field = $(field) if (!$field.hasClass('disabled-field')) { @@ -268,7 +284,7 @@ export default class Helpers { formData.push(fieldData) } }) - }) + } } return formData @@ -764,7 +780,6 @@ export default class Helpers { $(_this.d.stage) .find('li.form-field') .each((i, elem) => { - console.log(elem.id) this.closeField(elem.id, false) }) } From 76c8831015554dfb27d89e0fab4418510a0f084a Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 17:28:53 -0500 Subject: [PATCH 14/56] remove console --- src/js/helpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index a98d04f9e..453d2b835 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -209,7 +209,6 @@ export default class Helpers { forEach(form.childNodes, function (_index, testElement) { const $testElement = $(testElement) if ($testElement.is('li') && $testElement.hasClass('form-field')) { - console.log('adding open field ' + $testElement.attr('id')) fields.push(testElement) } }) From cf5efe13447beb230e574cd90252e538c34df60b Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 19:56:15 -0500 Subject: [PATCH 15/56] refactor --- src/js/layout.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/js/layout.js b/src/js/layout.js index 110f2823c..2596fc8b9 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -16,10 +16,7 @@ const processClassName = (data, field) => { // Now that the col- types were lifted, remove from the actual input field if (field.classList) { - for (let index = 0; index < classes.length; index++) { - const element = classes[index] - field.classList.remove(element) - } + field.classList.remove(...classes) } } From 63c1d66b14d7fe97509b83069886f7ce2c79b8a3 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 20:52:41 -0500 Subject: [PATCH 16/56] refactor regex and class search tests --- src/js/helpers.js | 12 +++++------- src/js/layout.js | 4 ++-- src/js/utils.js | 30 +++++++++++++++++------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index 453d2b835..f7fbec984 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' @@ -254,9 +256,7 @@ export default class Helpers { $field.closest('.rowWrapper').children().length == 1 && fieldData.className.includes(config.opts.defaultGridColumnClass) ) { - const classes = fieldData.className - .split(' ') - .filter(className => /^col-(xs|sm|md|lg)-([^\s]+)/.test(className) || className.startsWith('row-')) + const classes = getAllGridRelatedClasses(fieldData.className) if (classes && classes.length > 0) { classes.forEach(element => { @@ -1297,9 +1297,7 @@ export default class Helpers { const result = {} if (data.className) { - const classes = data.className - .split(' ') - .filter(className => /^col-(xs|sm|md|lg)-([^\s]+)/.test(className) || className.startsWith('row-')) + const classes = getAllGridRelatedClasses(data.className) if (classes && classes.length > 0) { classes.forEach(element => { @@ -1406,7 +1404,7 @@ export default class Helpers { return } - const splitClasses = className.split(' ').filter(className => /^col-(xs|sm|md|lg)-([^\s]+)/.test(className)) + const splitClasses = className.split(' ').filter(className => bootstrapColumnRegex.test(className)) if (splitClasses && splitClasses.length > 0) { return splitClasses[0] } diff --git a/src/js/layout.js b/src/js/layout.js index 2596fc8b9..976c57842 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -1,14 +1,14 @@ // 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(' ')}` diff --git a/src/js/utils.js b/src/js/utils.js index 35ee89c00..4317ffbf4 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) @@ -302,7 +302,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++) { @@ -393,7 +393,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 } @@ -505,9 +505,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 @@ -521,7 +519,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() }) } @@ -603,7 +601,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) } @@ -634,6 +632,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 From bfeb64b79914bf746c9213ae5d9f279fa4b8a946 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 17 Jan 2022 20:57:57 -0500 Subject: [PATCH 17/56] Update src/js/helpers.js Co-authored-by: Kevin Chappell --- src/js/helpers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index f7fbec984..ed257069b 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -846,7 +846,8 @@ export default class Helpers { document.dispatchEvent(events.fieldEditClosed) this.toggleGridModeButtonVisible(fieldID) - setTimeout(() => { + const resultsTimeout = setTimeout(() => { + clearTimeout(resultsTimeout) const cleanResults = _this.tmpCleanPrevHolder(prevHolder) cleanResults.forEach(result => { From 820b09e04cdb3689827090809593f3fff880ee37 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 18 Jan 2022 17:59:13 -0500 Subject: [PATCH 18/56] Fix empty container when removing field --- src/js/form-builder.js | 10 +++++++++- src/js/helpers.js | 27 +++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index eff2a4bfb..6a95dcd02 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -37,6 +37,7 @@ const FormBuilder = function (opts, element, $) { const data = new Data(formID) const d = new Dom(formID) let formRows = [] + formBuilder.preserveTempContainers = [] // prepare a new layout object with appropriate templates if (!opts.layout) { @@ -1027,7 +1028,7 @@ const FormBuilder = function (opts, element, $) { //Add a wrapper div for the field itself. This div will be the rendered representation const rowGroupNode2 = m('div', null, { id: `${field.id}-cont`, - className: columnData.columnSize, + className: `${columnData.columnSize} colWrapper`, }) $(rowGroupNode2).appendTo(rowWrapperNode) @@ -1599,6 +1600,13 @@ const FormBuilder = function (opts, element, $) { }) function checkRowCleanup() { + $stage.find('.colWrapper').each((i, elem) => { + const $colWrapper = $(elem) + if ($colWrapper.is(':empty') && !formBuilder.preserveTempContainers.includes($colWrapper.attr('id'))) { + $colWrapper.remove() + } + }) + $stage.children('.rowWrapper').each((i, elem) => { if ($(elem).children().length == 0) { const rowValue = h.getRowValue($(elem).attr('class')) diff --git a/src/js/helpers.js b/src/js/helpers.js index ed257069b..a54b76f1b 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -842,9 +842,10 @@ export default class Helpers { //Put the li back in its place rowContainer.append(liContainer) + this.removeContainerProtection(rowContainer.attr('id')) + config.opts.onCloseFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditClosed) - this.toggleGridModeButtonVisible(fieldID) const resultsTimeout = setTimeout(() => { clearTimeout(resultsTimeout) @@ -855,7 +856,7 @@ export default class Helpers { 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) + rowContainer.attr('class', `${result['columnInfo'].columnSize} colWrapper`) _this.tmpCleanPrevHolder(prevHolder) } } @@ -900,13 +901,15 @@ export default class Helpers { const liContainer = $(`#${fieldID}`) const rowContainer = $(`#${fieldID}-cont`) + //Mark the container as something we don't want to cleanup immediately + this.formBuilder.preserveTempContainers.push(rowContainer.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(rowContainer.closest('.rowWrapper')) this.formBuilder.currentEditPanel = $editPanel[0] config.opts.onOpenFieldEdit($editPanel[0]) document.dispatchEvent(events.fieldEditOpened) - this.toggleGridModeButtonVisible(fieldID, false) return field } @@ -1051,7 +1054,12 @@ export default class Helpers { document.dispatchEvent(events.fieldRemoved) - $(document).trigger('checkRowCleanup') + this.removeContainerProtection(`${fieldID}-cont`) + + setTimeout(() => { + $(document).trigger('checkRowCleanup') + }, 300) + return fieldRemoved } @@ -1314,13 +1322,12 @@ export default class Helpers { return result } - toggleGridModeButtonVisible(fieldId, visible = true) { - if (!visible) { - $(`#${fieldId}-grid`).css('display', 'none') - return + //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) } - - $(`#${fieldId}-grid`).css('display', '') } //Briefly highlight on/off From bed8ce5cfc31ca61b88cf74d961d9bc491dabad9 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 18 Jan 2022 18:01:39 -0500 Subject: [PATCH 19/56] Fixes when using the copy button --- src/js/form-builder.js | 39 +++++++++++++++++++++++++++++++++++---- src/js/helpers.js | 13 +++++-------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 6a95dcd02..e4d00f6af 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1375,11 +1375,42 @@ const FormBuilder = function (opts, element, $) { evt.preventDefault() const currentItem = $(evt.target).parent().parent('li') const $clone = cloneItem(currentItem) - $clone.insertAfter(currentItem) + prepareCloneWrappers($clone, currentItem) h.updatePreview($clone) h.save.call(h) + + h.tmpCleanPrevHolder($clone.find('.prev-holder')) }) + function prepareCloneWrappers($clone, currentItem) { + const inputClassElement = $(`#className-${currentItem.attr('id')}`) + const columnData = prepareFieldRow({}) + + const rowWrapper = m('div', null, { + id: `${$clone.attr('id')}-row`, + className: `row row-${columnData.rowNumber} rowWrapper`, + }) + + const colWrapper = m('div', null, { + id: `${$clone.attr('id')}-cont`, + className: `${h.getBootstrapColumnClass(inputClassElement.val())} colWrapper`, + }) + $(colWrapper).appendTo(rowWrapper) + + let insertAfterElement + if (currentItem.parent().is('div')) { + insertAfterElement = currentItem.closest('.rowWrapper') + } else if (currentItem.parent().is('ul')) { + insertAfterElement = currentItem.prev('.rowWrapper') + } + + $(rowWrapper).insertAfter(insertAfterElement) + $clone.appendTo(colWrapper) + + setupSortableWrapper(rowWrapper) + syncFieldWithNewRow($clone.attr('id')) + } + // Delete field $stage.on('click touchstart', '.delete-confirm', e => { e.preventDefault() @@ -1678,9 +1709,9 @@ const FormBuilder = function (opts, element, $) { Resize all fields within the row to be maximally equal - E - Enter Grid Mode when hovering over a form field - + E + Enter Grid Mode when hovering over a form field + diff --git a/src/js/helpers.js b/src/js/helpers.js index a54b76f1b..746ae66d4 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -834,10 +834,8 @@ export default class Helpers { } this.updatePreview($(field)) - const prevHolder = $(field).find('.prev-holder') - const fieldID = prevHolder.attr('data-field-id') - const liContainer = $(`#${fieldID}`) - const rowContainer = $(`#${fieldID}-cont`) + const liContainer = $(`#${fieldId}`) + const rowContainer = $(`#${fieldId}-cont`) //Put the li back in its place rowContainer.append(liContainer) @@ -847,6 +845,7 @@ export default class Helpers { 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) @@ -896,10 +895,8 @@ export default class Helpers { } this.updatePreview($(field)) - const prevHolder = $(field).find('.prev-holder') - const fieldID = prevHolder.attr('data-field-id') - const liContainer = $(`#${fieldID}`) - const rowContainer = $(`#${fieldID}-cont`) + const liContainer = $(`#${fieldId}`) + const rowContainer = $(`#${fieldId}-cont`) //Mark the container as something we don't want to cleanup immediately this.formBuilder.preserveTempContainers.push(rowContainer.attr('id')) From 390ebbaff294358cf5a0849b3fdab9999d55b9d5 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 18 Jan 2022 18:43:35 -0500 Subject: [PATCH 20/56] fix for remove all fields --- src/js/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/helpers.js b/src/js/helpers.js index 746ae66d4..09814c026 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -715,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('.rowWrapper') const markEmptyArray = [] if (!fields.length) { From 25c4b08259f1a91377c1b34ff02ebc836249db78 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 20 Jan 2022 18:17:49 -0500 Subject: [PATCH 21/56] Support dropping controls to a new row or column Update visuals --- src/js/form-builder.js | 336 +++++++++++++++++++++++++++++++++++------ src/js/helpers.js | 8 +- src/sass/_stage.scss | 42 +++++- 3 files changed, 334 insertions(+), 52 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index e4d00f6af..3f40c3ce2 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -29,6 +29,19 @@ 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 tmpRowWrapperClassSelector = '.tempRowWrapper' +const tmpRowWrapperClass = tmpRowWrapperClassSelector.replace('.', '') + +const tmpColWrapperClassSelector = '.tempColWrapper' +const tmpColWrapperClass = tmpColWrapperClassSelector.replace('.', '') + +let isMoving = false const FormBuilder = function (opts, element, $) { const formBuilder = this @@ -36,8 +49,12 @@ const FormBuilder = function (opts, element, $) { const formID = `frmb-${new Date().getTime()}` 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) { @@ -63,28 +80,211 @@ const FormBuilder = function (opts, element, $) { $stage.sortable('disable') } + let droppingNewControl = false + let dropTargetIsRow = false + let dropTargetIsColumn = false + + let $targetDropWrapper + + //Setup areas to connect/drag a control with + $cbUL.hover( + function () { + if (isMoving) { + return + } + + //Drop to create new row above/below an existing field + SetupDroppableRows() + + //Drop area to merge field into row to the left/right of existing field + SetupDroppableColumns() + }, + function () { + if (!isMoving) { + cleanupDropAreas() + } + }, + ) + + function cleanupDropAreas(hard = false) { + //Cleanup after moving hover away + $stage.find(tmpRowWrapperClassSelector).css('display', 'none') + $stage.find(tmpColWrapperClassSelector).css('display', 'none') + + $stage.find(colWrapperClassSelector).removeClass('colHoverTempStyle') + + if (hard) { + $stage.find(tmpRowWrapperClassSelector).remove() + $stage.find(tmpColWrapperClassSelector).remove() + } + } + + //Turn off drop areas during scrolling so the fixed cols dont look odd + $(window).scroll(function () { + cleanupDropAreas() + }) + + function SetupDroppableRows() { + $stage.find(tmpRowWrapperClassSelector).remove() + + $stage.children(rowWrapperClassSelector).each((i, elem) => { + const rowWrapper = $(elem) + + const tmpRowTarget = m('div', null, { + className: tmpRowWrapperClass, + }) + $(tmpRowTarget).addClass('hoverDropStyle') + + if (rowWrapper.index() == 0) { + const beforeClone = $(tmpRowTarget).clone() + beforeClone.insertBefore(rowWrapper) + setupDroppableRow(beforeClone) + } + + $(tmpRowTarget).insertAfter(rowWrapper) + setupDroppableRow($(tmpRowTarget)) + }) + } + + function setupDroppableRow(element) { + $(element).sortable({ + over: function (event) { + $(event.target).addClass('hoverDropStyleInverse') + }, + out: function (event) { + $(event.target).removeClass('hoverDropStyleInverse') + }, + receive: function (event, ui) { + if (droppingNewControl) { + dropTargetIsRow = true + + $targetDropWrapper = $(ui.item.parent()) + h.doCancel = true + processControl(ui.item) + } + }, + deactivate: function () { + $stage.find(tmpRowWrapperClassSelector).remove() + }, + }) + } + + function SetupDroppableColumns() { + $stage.find(tmpColWrapperClassSelector).remove() + + $stage.children(rowWrapperClassSelector).each((i, rowWrapper) => { + //Get the max height of the entire row to set the placeholder height as + let maxColumnHeight = 10 + $(rowWrapper) + .children(colWrapperClassSelector) + .each((i, elem) => { + const colWrapper = $(elem) + const colHeight = parseInt(colWrapper.css('height')) + if (colHeight > maxColumnHeight) { + maxColumnHeight = colHeight + } + }) + + $(rowWrapper) + .children(colWrapperClassSelector) + .each((i, elem) => { + const colWrapper = $(elem) + colWrapper.addClass('colHoverTempStyle') + + const tmpColTarget = m('div', null, { + className: tmpColWrapperClass, + }) + + if (colWrapper.index() == 0) { + const beforeClone = $(tmpColTarget).clone() + beforeClone + .addClass('hoverColumnDropStyle') + .css({ left: 0, top: colWrapper.offset().top - $(window).scrollTop(), height: maxColumnHeight }) + beforeClone.insertBefore(colWrapper) + setupDroppableColumn(beforeClone) + } + + $(tmpColTarget) + .addClass('hoverColumnDropStyle') + .css({ + left: colWrapper.offset().left + colWrapper.width() + 40, + top: colWrapper.offset().top - $(window).scrollTop(), + height: maxColumnHeight, + }) + + $(tmpColTarget).insertAfter(colWrapper) + setupDroppableColumn($(tmpColTarget)) + }) + }) + } + + function setupDroppableColumn(element) { + $(element).sortable({ + over: function (event) { + $(event.target).addClass('hoverDropStyleInverse') + }, + out: function (event) { + $(event.target).removeClass('hoverDropStyleInverse') + }, + receive: function (event, ui) { + if (droppingNewControl) { + dropTargetIsColumn = true + $targetDropWrapper = $(ui.item.parent()) + h.doCancel = true + processControl(ui.item) + } + }, + deactivate: function () { + $stage.find(tmpColWrapperClassSelector).remove() + }, + }) + } + // ControlBox with different fields $cbUL.sortable({ - helper: 'clone', + helper: function (e, el) { + //Shrink the control a little while dragging so it's not in the way as much + return el + .clone() + .css({ width: '50px', height: '35px', border: '1px', borderStyle: 'solid', borderColor: 'black' }) + .html('') + }, opacity: 0.9, - connectWith: $stage, + connectWith: [tmpRowWrapperClassSelector, tmpColWrapperClassSelector], cancel: '.formbuilder-separator', cursor: 'move', + cursorAt: { + left: 5, + top: 5, + }, + containment: 'window', scroll: false, - placeholder: 'ui-state-highlight', - start: (evt, ui) => h.startMoving.call(h, evt, ui), - stop: (evt, ui) => h.stopMoving.call(h, evt, ui), + start: (evt, ui) => { + h.startMoving.call(h, evt, ui) + isMoving = true + }, + stop: (evt, ui) => { + h.stopMoving.call(h, evt, ui) + isMoving = false + cleanupDropAreas(true) + }, revert: 150, - beforeStop: (evt, ui) => h.beforeStop.call(h, evt, ui), + beforeStop: (evt, ui) => { + h.beforeStop.call(h, evt, ui) + }, distance: 3, update: function (event, ui) { + isMoving = false if (h.doCancel) { return false } - if (ui.item.parent()[0] === d.stage) { - h.doCancel = true - processControl(ui.item) + dropTargetIsColumn = $(ui.item.parent()).hasClass(tmpColWrapperClass) + dropTargetIsRow = $(ui.item.parent()).hasClass(tmpRowWrapperClass) + const dropTargetIsStage = ui.item.parent().parent()[0] === d.stage + + if (dropTargetIsStage || dropTargetIsRow || dropTargetIsColumn) { + droppingNewControl = true } else { h.setFieldOrder($cbUL) h.doCancel = !opts.sortableControls @@ -1012,7 +1212,13 @@ const FormBuilder = function (opts, element, $) { // generate the control, insert it into the list item & add it to the stage h.updatePreview($li) - const targetRow = `div.row-${columnData.rowNumber}` + let targetRow = `div.row-${columnData.rowNumber}` + + //If dropping to column area, use the row that already exists + if (droppingNewControl && dropTargetIsColumn) { + targetRow = `div.${h.getRowClass($targetDropWrapper.parent().attr('class'))}` + } + let rowWrapperNode //Check if an overall row already exists for the field, else create one @@ -1021,21 +1227,45 @@ const FormBuilder = function (opts, element, $) { } else { rowWrapperNode = m('div', null, { id: `${field.id}-row`, - className: `row row-${columnData.rowNumber} rowWrapper`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, }) } + //Turn the placeholder into the new row. Copy some attributes over + if (droppingNewControl && dropTargetIsRow) { + $targetDropWrapper.attr('id', rowWrapperNode.id) + $targetDropWrapper.attr('class', rowWrapperNode.className) + $targetDropWrapper.attr('style', '') + rowWrapperNode = $targetDropWrapper + } + //Add a wrapper div for the field itself. This div will be the rendered representation - const rowGroupNode2 = m('div', null, { + let rowGroupNode2 = m('div', null, { id: `${field.id}-cont`, - className: `${columnData.columnSize} colWrapper`, + className: `${columnData.columnSize} ${colWrapperClass}`, }) - $(rowGroupNode2).appendTo(rowWrapperNode) - $stage.append(rowWrapperNode) + //Turn the placeholder into the new column wrapper. Copy some attributes over + if (droppingNewControl && dropTargetIsColumn) { + $targetDropWrapper.attr('id', rowGroupNode2.id) + $targetDropWrapper.attr('class', rowGroupNode2.className) + $targetDropWrapper.attr('style', '') + rowGroupNode2 = $targetDropWrapper + } + + //If dropping, use the existing index, do not always append to end + if (!dropTargetIsColumn) { + $(rowGroupNode2).appendTo(rowWrapperNode) + } + + //If dropping, use the existing index, do not always append to end + if (!droppingNewControl) { + $stage.append(rowWrapperNode) + } + $li.appendTo(rowGroupNode2) - setupSortableWrapper(rowWrapperNode) + setupSortableRowWrapper(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 @@ -1058,22 +1288,40 @@ const FormBuilder = function (opts, element, $) { field.scrollIntoView({ behavior: 'smooth' }) } } + + //Autosize entire row when dropping from control area + if (droppingNewControl && dropTargetIsColumn) { + autoSizeRowColumns(rowWrapperNode, true) + } + + cleanupDropAreas(true) + + droppingNewControl = false + dropTargetIsRow = false + dropTargetIsColumn = false } - function setupSortableWrapper(rowWrapperNode) { + function setupSortableRowWrapper(rowWrapperNode) { $(rowWrapperNode).sortable({ - connectWith: '.rowWrapper', + connectWith: rowWrapperClassSelector, cursor: 'move', opacity: 0.9, revert: 150, - cursorAt: { - left: 5, - top: 5, + deactivate: function () { + cleanupDropAreas(true) }, placeholder: 'ui-state-highlight', grid: [1, 1], + receive: function (event, ui) { + cleanupDropAreas(true) + + if (droppingNewControl) { + h.doCancel = true + processControl(ui.item) + } + }, stop: function (event, ui) { - autoSizeRowColumns(ui.item.closest('.rowWrapper')) + autoSizeRowColumns(ui.item.closest(rowWrapperClassSelector)) }, update: function (event, ui) { syncFieldWithNewRow(ui.item.attr('id')) @@ -1388,26 +1636,26 @@ const FormBuilder = function (opts, element, $) { const rowWrapper = m('div', null, { id: `${$clone.attr('id')}-row`, - className: `row row-${columnData.rowNumber} rowWrapper`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, }) const colWrapper = m('div', null, { id: `${$clone.attr('id')}-cont`, - className: `${h.getBootstrapColumnClass(inputClassElement.val())} colWrapper`, + className: `${h.getBootstrapColumnClass(inputClassElement.val())} ${colWrapperClass}`, }) $(colWrapper).appendTo(rowWrapper) let insertAfterElement if (currentItem.parent().is('div')) { - insertAfterElement = currentItem.closest('.rowWrapper') + insertAfterElement = currentItem.closest(rowWrapperClassSelector) } else if (currentItem.parent().is('ul')) { - insertAfterElement = currentItem.prev('.rowWrapper') + insertAfterElement = currentItem.prev(rowWrapperClassSelector) } $(rowWrapper).insertAfter(insertAfterElement) $clone.appendTo(colWrapper) - setupSortableWrapper(rowWrapper) + setupSortableRowWrapper(rowWrapper) syncFieldWithNewRow($clone.attr('id')) } @@ -1496,6 +1744,7 @@ const FormBuilder = function (opts, element, $) { gridModeTargetField.attr('manuallyChangedDefaultColumnClass', true) buildGridModeCurrentRowInfo() + h.toggleHighlight(gridModeTargetField) } }) @@ -1504,7 +1753,7 @@ const FormBuilder = function (opts, element, $) { $(document).keydown(e => { if (gridMode) { e.preventDefault() - const rowWrapper = gridModeTargetField.closest('.rowWrapper') + const rowWrapper = gridModeTargetField.closest(rowWrapperClassSelector) if (e.keyCode == 87 || e.keyCode == 38) { moveFieldUp(rowWrapper) @@ -1573,7 +1822,7 @@ const FormBuilder = function (opts, element, $) { const rowWrapperNode = m('div', null, { id: `${gridModeTargetField.attr('id')}-row`, - className: `row row-${columnData.rowNumber} rowWrapper`, + className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, }) gridModeTargetField.parent().appendTo(rowWrapperNode) @@ -1584,16 +1833,16 @@ const FormBuilder = function (opts, element, $) { $stage.append(rowWrapperNode) } - setupSortableWrapper(rowWrapperNode) + setupSortableRowWrapper(rowWrapperNode) syncFieldWithNewRow(gridModeTargetField.attr('id')) checkRowCleanup() } function autoSizeRowColumns(rowWrapper, force = false) { - const childRowCount = rowWrapper.children('div').length + const childRowCount = rowWrapper.children(`div${colWrapperClassSelector}`).length const newAutoCalcSizeValue = Math.floor(12 / childRowCount) - rowWrapper.children('div').each((i, elem) => { + 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 @@ -1607,12 +1856,14 @@ const FormBuilder = function (opts, element, $) { } function syncFieldWithNewRow(fieldID) { - const inputClassElement = $(`#className-${fieldID.replace('-cont', '')}`) - if (inputClassElement.val()) { - const oldRow = h.getRowClass(inputClassElement.val()) - const wrapperRow = h.getRowClass(inputClassElement.closest('.rowWrapper').attr('class')) - inputClassElement.val(inputClassElement.val().replace(oldRow, wrapperRow)) - checkRowCleanup() + 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() + } } } @@ -1631,14 +1882,14 @@ const FormBuilder = function (opts, element, $) { }) function checkRowCleanup() { - $stage.find('.colWrapper').each((i, elem) => { + $stage.find(colWrapperClassSelector).each((i, elem) => { const $colWrapper = $(elem) if ($colWrapper.is(':empty') && !formBuilder.preserveTempContainers.includes($colWrapper.attr('id'))) { $colWrapper.remove() } }) - $stage.children('.rowWrapper').each((i, elem) => { + $stage.children(rowWrapperClassSelector).each((i, elem) => { if ($(elem).children().length == 0) { const rowValue = h.getRowValue($(elem).attr('class')) formRows = formRows.filter(x => x != rowValue) @@ -1658,6 +1909,7 @@ const FormBuilder = function (opts, element, $) { buildGridModeHelp() h.closeAllEdit() + h.toggleHighlight(gridModeTargetField) } else { h.showToast('Grid Mode Finished', 1500) gridMode = false @@ -1743,9 +1995,9 @@ const FormBuilder = function (opts, element, $) { function buildGridModeCurrentRowInfo() { $(gridModeHelp).find('.gridHelpCurrentRow tbody').empty() - const rowWrapper = gridModeTargetField.closest('.rowWrapper') + const rowWrapper = gridModeTargetField.closest(rowWrapperClassSelector) - rowWrapper.children('div').each((i, elem) => { + rowWrapper.children(`div${colWrapperClassSelector}`).each((i, elem) => { const colWrapper = $(`#${elem.id}`) const fieldID = colWrapper.find('li').attr('id') const label = $(`#label-${fieldID}`).html() diff --git a/src/js/helpers.js b/src/js/helpers.js index 09814c026..7a21f3e3c 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -253,7 +253,7 @@ export default class Helpers { if ( fieldData.className && $field.attr('addeddefaultcolumnclass') == 'true' && - $field.closest('.rowWrapper').children().length == 1 && + $field.closest(this.formBuilder.rowWrapperClassSelector).children().length == 1 && fieldData.className.includes(config.opts.defaultGridColumnClass) ) { const classes = getAllGridRelatedClasses(fieldData.className) @@ -715,7 +715,7 @@ export default class Helpers { removeAllFields(stage) { const i18n = mi18n.current const opts = config.opts - const fields = stage.querySelectorAll('.rowWrapper') + const fields = stage.querySelectorAll(this.formBuilder.rowWrapperClassSelector) const markEmptyArray = [] if (!fields.length) { @@ -855,7 +855,7 @@ export default class Helpers { 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} colWrapper`) + rowContainer.attr('class', `${result['columnInfo'].columnSize} ${this.formBuilder.colWrapperClass}`) _this.tmpCleanPrevHolder(prevHolder) } } @@ -902,7 +902,7 @@ export default class Helpers { this.formBuilder.preserveTempContainers.push(rowContainer.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(rowContainer.closest('.rowWrapper')) + liContainer.insertAfter(rowContainer.closest(this.formBuilder.rowWrapperClassSelector)) this.formBuilder.currentEditPanel = $editPanel[0] config.opts.onOpenFieldEdit($editPanel[0]) diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 0fe55814c..36b2b2d6a 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -801,18 +801,20 @@ /* ------------ END TOOLTIP ------------ */ .ui-state-highlight { - border-color: #00ccff; - background-color: #00ccff; - width: 10px; - border-radius: 6px; + border-radius: 3px; + border: 1px dashed #0d99f2; + border-radius: 3px; + background-color: #e5f5f8; + width: 12px; } .moveHighlight { - background-color: rgba(171, 234, 250, 0.664) !important; + border: 1px dashed #0d99f2 !important; + background-color: #e5f5f8 !important; } .currentGridModeFieldHighlight { - background-color: rgba(171, 234, 250, 0.664) !important; + background-color: #e5f5f8 !important; } .grid-mode-help { @@ -830,3 +832,31 @@ .grid-mode-help-row2 { white-space: nowrap; } + +.hoverDropStyle { + border: 1px dashed #0d99f2; + border-radius: 3px; + background-color: #e5f5f8; + height: 20px; + margin: 6px; +} + +.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; +} + +.colHoverTempStyle { + margin-right: 80px !important; + margin-left: 70px !important; + flex: 95 !important; +} From 403ed1bae01e2fc5451aa3ac2451c5faade9fa54 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 20 Jan 2022 19:44:46 -0500 Subject: [PATCH 22/56] add configuration for row/column drop features --- src/js/config.js | 2 ++ src/js/form-builder.js | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index 9bf406bf8..efd8603c9 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -86,6 +86,8 @@ export const defaultOptions = { typeUserEvents: {}, defaultGridColumnClass: 'col-md-12', cancelGridModeDistance: 100, + enableRowDrop: false, + enableColumnDrop: false, } export const styles = { diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 3f40c3ce2..f3f6aa912 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -94,10 +94,14 @@ const FormBuilder = function (opts, element, $) { } //Drop to create new row above/below an existing field - SetupDroppableRows() + if (config.opts.enableRowDrop) { + SetupDroppableRows() + } //Drop area to merge field into row to the left/right of existing field - SetupDroppableColumns() + if (config.opts.enableColumnDrop) { + SetupDroppableColumns() + } }, function () { if (!isMoving) { @@ -107,6 +111,10 @@ const FormBuilder = function (opts, element, $) { ) function cleanupDropAreas(hard = false) { + if (!config.opts.enableRowDrop && !config.opts.enableColumnDrop) { + return + } + //Cleanup after moving hover away $stage.find(tmpRowWrapperClassSelector).css('display', 'none') $stage.find(tmpColWrapperClassSelector).css('display', 'none') From 2bfea1cdb1adb6b4f800929fdd47e49a07633046 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 20 Jan 2022 21:30:27 -0500 Subject: [PATCH 23/56] temp fix for scrolling --- src/js/form-builder.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index f3f6aa912..860e6b6b2 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -129,7 +129,9 @@ const FormBuilder = function (opts, element, $) { //Turn off drop areas during scrolling so the fixed cols dont look odd $(window).scroll(function () { - cleanupDropAreas() + if (config.opts.enableColumnDrop) { + cleanupDropAreas() + } }) function SetupDroppableRows() { @@ -265,7 +267,6 @@ const FormBuilder = function (opts, element, $) { left: 5, top: 5, }, - containment: 'window', scroll: false, start: (evt, ui) => { h.startMoving.call(h, evt, ui) From 5b4956916cd6fcf2e18ebcc8178c7e437bd944b4 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 21 Jan 2022 17:37:29 -0500 Subject: [PATCH 24/56] Rethink column insert. Make it cleaner and more productive --- src/js/config.js | 1 - src/js/form-builder.js | 246 +++++++++++++++++++-------------------- src/js/helpers.js | 6 + src/sass/_stage.scss | 14 ++- src/sass/base/_font.scss | 105 ++++++++++++----- 5 files changed, 212 insertions(+), 160 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index efd8603c9..641109342 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -87,7 +87,6 @@ export const defaultOptions = { defaultGridColumnClass: 'col-md-12', cancelGridModeDistance: 100, enableRowDrop: false, - enableColumnDrop: false, } export const styles = { diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 860e6b6b2..e6985a3e0 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -80,11 +80,12 @@ const FormBuilder = function (opts, element, $) { $stage.sortable('disable') } - let droppingNewControl = false - let dropTargetIsRow = false - let dropTargetIsColumn = false + let insertingNewControl = false + let insertTargetIsRow = false + let insertTargetIsColumn = false - let $targetDropWrapper + let $targetInsertWrapper + let cloneControls //Setup areas to connect/drag a control with $cbUL.hover( @@ -97,21 +98,16 @@ const FormBuilder = function (opts, element, $) { if (config.opts.enableRowDrop) { SetupDroppableRows() } - - //Drop area to merge field into row to the left/right of existing field - if (config.opts.enableColumnDrop) { - SetupDroppableColumns() - } }, function () { if (!isMoving) { - cleanupDropAreas() + cleanupTempPlaceholders() } }, ) - function cleanupDropAreas(hard = false) { - if (!config.opts.enableRowDrop && !config.opts.enableColumnDrop) { + function cleanupTempPlaceholders(hard = false) { + if (!config.opts.enableRowDrop) { return } @@ -127,13 +123,6 @@ const FormBuilder = function (opts, element, $) { } } - //Turn off drop areas during scrolling so the fixed cols dont look odd - $(window).scroll(function () { - if (config.opts.enableColumnDrop) { - cleanupDropAreas() - } - }) - function SetupDroppableRows() { $stage.find(tmpRowWrapperClassSelector).remove() @@ -165,10 +154,10 @@ const FormBuilder = function (opts, element, $) { $(event.target).removeClass('hoverDropStyleInverse') }, receive: function (event, ui) { - if (droppingNewControl) { - dropTargetIsRow = true + if (insertingNewControl) { + insertTargetIsRow = true - $targetDropWrapper = $(ui.item.parent()) + $targetInsertWrapper = $(ui.item.parent()) h.doCancel = true processControl(ui.item) } @@ -179,75 +168,27 @@ const FormBuilder = function (opts, element, $) { }) } - function SetupDroppableColumns() { - $stage.find(tmpColWrapperClassSelector).remove() - - $stage.children(rowWrapperClassSelector).each((i, rowWrapper) => { - //Get the max height of the entire row to set the placeholder height as - let maxColumnHeight = 10 - $(rowWrapper) - .children(colWrapperClassSelector) - .each((i, elem) => { - const colWrapper = $(elem) - const colHeight = parseInt(colWrapper.css('height')) - if (colHeight > maxColumnHeight) { - maxColumnHeight = colHeight - } - }) - - $(rowWrapper) - .children(colWrapperClassSelector) - .each((i, elem) => { - const colWrapper = $(elem) - colWrapper.addClass('colHoverTempStyle') - - const tmpColTarget = m('div', null, { - className: tmpColWrapperClass, - }) - - if (colWrapper.index() == 0) { - const beforeClone = $(tmpColTarget).clone() - beforeClone - .addClass('hoverColumnDropStyle') - .css({ left: 0, top: colWrapper.offset().top - $(window).scrollTop(), height: maxColumnHeight }) - beforeClone.insertBefore(colWrapper) - setupDroppableColumn(beforeClone) - } - - $(tmpColTarget) - .addClass('hoverColumnDropStyle') - .css({ - left: colWrapper.offset().left + colWrapper.width() + 40, - top: colWrapper.offset().top - $(window).scrollTop(), - height: maxColumnHeight, - }) - - $(tmpColTarget).insertAfter(colWrapper) - setupDroppableColumn($(tmpColTarget)) - }) - }) - } - - function setupDroppableColumn(element) { - $(element).sortable({ - over: function (event) { - $(event.target).addClass('hoverDropStyleInverse') - }, - out: function (event) { - $(event.target).removeClass('hoverDropStyleInverse') - }, - receive: function (event, ui) { - if (droppingNewControl) { - dropTargetIsColumn = true - $targetDropWrapper = $(ui.item.parent()) - h.doCancel = true - processControl(ui.item) + function setupColumnInserts(rowWrapper) { + $(rowWrapper) + .children(colWrapperClassSelector) + .each((i, elem) => { + const colWrapper = $(elem) + colWrapper.addClass('colHoverTempStyle') + + if (colWrapper.index() == 0) { + $( + ``, + ).insertBefore(colWrapper) } - }, - deactivate: function () { - $stage.find(tmpColWrapperClassSelector).remove() - }, - }) + + $( + ``, + ).insertAfter(colWrapper) + }) } // ControlBox with different fields @@ -275,7 +216,7 @@ const FormBuilder = function (opts, element, $) { stop: (evt, ui) => { h.stopMoving.call(h, evt, ui) isMoving = false - cleanupDropAreas(true) + cleanupTempPlaceholders(true) }, revert: 150, beforeStop: (evt, ui) => { @@ -288,12 +229,11 @@ const FormBuilder = function (opts, element, $) { return false } - dropTargetIsColumn = $(ui.item.parent()).hasClass(tmpColWrapperClass) - dropTargetIsRow = $(ui.item.parent()).hasClass(tmpRowWrapperClass) + insertTargetIsRow = $(ui.item.parent()).hasClass(tmpRowWrapperClass) const dropTargetIsStage = ui.item.parent().parent()[0] === d.stage - if (dropTargetIsStage || dropTargetIsRow || dropTargetIsColumn) { - droppingNewControl = true + if (dropTargetIsStage || insertTargetIsRow) { + insertingNewControl = true } else { h.setFieldOrder($cbUL) h.doCancel = !opts.sortableControls @@ -1221,12 +1161,7 @@ const FormBuilder = function (opts, element, $) { // generate the control, insert it into the list item & add it to the stage h.updatePreview($li) - let targetRow = `div.row-${columnData.rowNumber}` - - //If dropping to column area, use the row that already exists - if (droppingNewControl && dropTargetIsColumn) { - targetRow = `div.${h.getRowClass($targetDropWrapper.parent().attr('class'))}` - } + const targetRow = `div.row-${columnData.rowNumber}` let rowWrapperNode @@ -1241,34 +1176,34 @@ const FormBuilder = function (opts, element, $) { } //Turn the placeholder into the new row. Copy some attributes over - if (droppingNewControl && dropTargetIsRow) { - $targetDropWrapper.attr('id', rowWrapperNode.id) - $targetDropWrapper.attr('class', rowWrapperNode.className) - $targetDropWrapper.attr('style', '') - rowWrapperNode = $targetDropWrapper + 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 - let rowGroupNode2 = m('div', null, { + const rowGroupNode2 = m('div', null, { id: `${field.id}-cont`, className: `${columnData.columnSize} ${colWrapperClass}`, }) - //Turn the placeholder into the new column wrapper. Copy some attributes over - if (droppingNewControl && dropTargetIsColumn) { - $targetDropWrapper.attr('id', rowGroupNode2.id) - $targetDropWrapper.attr('class', rowGroupNode2.className) - $targetDropWrapper.attr('style', '') - rowGroupNode2 = $targetDropWrapper + if (insertingNewControl && insertTargetIsColumn) { + if ($targetInsertWrapper.attr('prepend') == 'true') { + $(rowGroupNode2).prependTo(rowWrapperNode) + } else { + $(rowGroupNode2).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) + } } - //If dropping, use the existing index, do not always append to end - if (!dropTargetIsColumn) { + //Control insert will take care of inserting itself + if (!insertTargetIsColumn) { $(rowGroupNode2).appendTo(rowWrapperNode) } - //If dropping, use the existing index, do not always append to end - if (!droppingNewControl) { + //If inserting, use the existing index, do not always append to end + if (!insertingNewControl) { $stage.append(rowWrapperNode) } @@ -1298,16 +1233,16 @@ const FormBuilder = function (opts, element, $) { } } - //Autosize entire row when dropping from control area - if (droppingNewControl && dropTargetIsColumn) { + //Autosize entire row when using new insert mode + if (insertingNewControl && insertTargetIsColumn) { autoSizeRowColumns(rowWrapperNode, true) } - cleanupDropAreas(true) + cleanupTempPlaceholders(true) - droppingNewControl = false - dropTargetIsRow = false - dropTargetIsColumn = false + insertingNewControl = false + insertTargetIsRow = false + insertTargetIsColumn = false } function setupSortableRowWrapper(rowWrapperNode) { @@ -1317,14 +1252,14 @@ const FormBuilder = function (opts, element, $) { opacity: 0.9, revert: 150, deactivate: function () { - cleanupDropAreas(true) + cleanupTempPlaceholders(true) }, placeholder: 'ui-state-highlight', grid: [1, 1], receive: function (event, ui) { - cleanupDropAreas(true) + cleanupTempPlaceholders(true) - if (droppingNewControl) { + if (insertingNewControl) { h.doCancel = true processControl(ui.item) } @@ -1336,6 +1271,17 @@ const FormBuilder = function (opts, element, $) { syncFieldWithNewRow(ui.item.attr('id')) }, }) + + $(rowWrapperNode).off('mouseenter') + $(rowWrapperNode).on('mouseenter', function (e) { + setupColumnInserts($(e.currentTarget)) + }) + + $(rowWrapperNode).off('mouseleave') + $(rowWrapperNode).on('mouseleave', function (e) { + $(e.currentTarget).find(tmpColWrapperClassSelector).remove() + $(e.currentTarget).find(colWrapperClassSelector).removeClass('colHoverTempStyle') + }) } function prepareFieldRow(data) { @@ -1361,6 +1307,12 @@ const FormBuilder = function (opts, element, $) { } 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) { @@ -1627,6 +1579,48 @@ 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(), + }) + } + }) + // Copy field $stage.on('click touchstart', `.${css_prefix_text}copy`, function (evt) { evt.preventDefault() diff --git a/src/js/helpers.js b/src/js/helpers.js index 7a21f3e3c..cf08f9c27 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -1379,6 +1379,12 @@ export default class Helpers { } } + //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) { diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 36b2b2d6a..e4d130bc5 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -856,7 +856,17 @@ } .colHoverTempStyle { - margin-right: 80px !important; - margin-left: 70px !important; + margin-right: 10px !important; + margin-left: 10px !important; flex: 95 !important; } + +.rowWrapper { + margin-left: 0px !important; + margin-right: 0px !important; +} + +.btnAddControl { + border: 0; + background-color: unset; +} diff --git a/src/sass/base/_font.scss b/src/sass/base/_font.scss index c237f4244..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,53 +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'; } /* '' */ -.formbuilder-icon-grid:before { content: url("data:image/svg+xml; utf8, "); } \ 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,"); +} From ace2644de03685846ad8345646ab0a03675838e0 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 21 Jan 2022 19:12:35 -0500 Subject: [PATCH 25/56] Do not allow resize over 12 --- src/js/form-builder.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index e6985a3e0..e0fd70d0e 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1743,6 +1743,24 @@ const FormBuilder = function (opts, element, $) { 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) From ba35619fdc1737ab32aff76d1a727c4ddbaaca03 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 21 Jan 2022 20:22:55 -0500 Subject: [PATCH 26/56] 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. --- src/js/form-builder.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index e0fd70d0e..ca344223c 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1933,6 +1933,21 @@ const FormBuilder = function (opts, element, $) { h.toggleHighlight(gridModeTargetField) } 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 From 0cb353464bd2911ee5310aec430d9f59b91f3d3d Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 21 Jan 2022 20:43:48 -0500 Subject: [PATCH 27/56] Remove column insert buttons when using grid mode --- src/js/form-builder.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index ca344223c..3aaabe02c 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1279,11 +1279,15 @@ const FormBuilder = function (opts, element, $) { $(rowWrapperNode).off('mouseleave') $(rowWrapperNode).on('mouseleave', function (e) { - $(e.currentTarget).find(tmpColWrapperClassSelector).remove() - $(e.currentTarget).find(colWrapperClassSelector).removeClass('colHoverTempStyle') + removeColumnInsertButtons($(e.currentTarget)) }) } + function removeColumnInsertButtons(rowWrapper) { + rowWrapper.find(tmpColWrapperClassSelector).remove() + rowWrapper.find(colWrapperClassSelector).removeClass('colHoverTempStyle') + } + function prepareFieldRow(data) { let result = {} @@ -1797,6 +1801,7 @@ const FormBuilder = function (opts, element, $) { } buildGridModeCurrentRowInfo() + removeColumnInsertButtons(rowWrapper) } }) From ad699b70913f9ab1dc40d59098defa01d05239f7 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 21 Jan 2022 21:25:44 -0500 Subject: [PATCH 28/56] Remove temp rows during grid mode --- src/js/form-builder.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 3aaabe02c..780153c19 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1920,6 +1920,8 @@ const FormBuilder = function (opts, element, $) { const rowValue = h.getRowValue($(elem).attr('class')) formRows = formRows.filter(x => x != rowValue) $(elem).remove() + } else { + removeColumnInsertButtons($(elem)) } }) } @@ -1933,6 +1935,9 @@ const FormBuilder = function (opts, element, $) { $cbUL.css('display', 'none') $(d.formActions).css('display', 'none') + //Cleanup temp artifacts + cleanupTempPlaceholders(true) + buildGridModeHelp() h.closeAllEdit() h.toggleHighlight(gridModeTargetField) From da873278a7597bae1707268cd139bb20975a183d Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 21 Jan 2022 23:44:04 -0500 Subject: [PATCH 29/56] Remove row placeholder concept. Revert to standard control drop. Minor other fixes. --- src/js/form-builder.js | 222 ++++++++++++++++------------------------- src/js/helpers.js | 3 +- 2 files changed, 89 insertions(+), 136 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 780153c19..04c67c504 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -35,9 +35,6 @@ const rowWrapperClass = rowWrapperClassSelector.replace('.', '') const colWrapperClassSelector = '.colWrapper' const colWrapperClass = colWrapperClassSelector.replace('.', '') -const tmpRowWrapperClassSelector = '.tempRowWrapper' -const tmpRowWrapperClass = tmpRowWrapperClassSelector.replace('.', '') - const tmpColWrapperClassSelector = '.tempColWrapper' const tmpColWrapperClass = tmpColWrapperClassSelector.replace('.', '') @@ -83,131 +80,30 @@ const FormBuilder = function (opts, element, $) { let insertingNewControl = false let insertTargetIsRow = false let insertTargetIsColumn = false + let insertTargetIsStage = false let $targetInsertWrapper let cloneControls - //Setup areas to connect/drag a control with - $cbUL.hover( - function () { - if (isMoving) { - return - } - - //Drop to create new row above/below an existing field - if (config.opts.enableRowDrop) { - SetupDroppableRows() - } - }, - function () { - if (!isMoving) { - cleanupTempPlaceholders() - } - }, - ) - - function cleanupTempPlaceholders(hard = false) { - if (!config.opts.enableRowDrop) { - return - } - - //Cleanup after moving hover away - $stage.find(tmpRowWrapperClassSelector).css('display', 'none') - $stage.find(tmpColWrapperClassSelector).css('display', 'none') - - $stage.find(colWrapperClassSelector).removeClass('colHoverTempStyle') - - if (hard) { - $stage.find(tmpRowWrapperClassSelector).remove() - $stage.find(tmpColWrapperClassSelector).remove() - } - } - - function SetupDroppableRows() { - $stage.find(tmpRowWrapperClassSelector).remove() - - $stage.children(rowWrapperClassSelector).each((i, elem) => { - const rowWrapper = $(elem) - - const tmpRowTarget = m('div', null, { - className: tmpRowWrapperClass, - }) - $(tmpRowTarget).addClass('hoverDropStyle') - - if (rowWrapper.index() == 0) { - const beforeClone = $(tmpRowTarget).clone() - beforeClone.insertBefore(rowWrapper) - setupDroppableRow(beforeClone) - } - - $(tmpRowTarget).insertAfter(rowWrapper) - setupDroppableRow($(tmpRowTarget)) - }) - } - - function setupDroppableRow(element) { - $(element).sortable({ - over: function (event) { - $(event.target).addClass('hoverDropStyleInverse') - }, - out: function (event) { - $(event.target).removeClass('hoverDropStyleInverse') - }, - receive: function (event, ui) { - if (insertingNewControl) { - insertTargetIsRow = true - - $targetInsertWrapper = $(ui.item.parent()) - h.doCancel = true - processControl(ui.item) - } - }, - deactivate: function () { - $stage.find(tmpRowWrapperClassSelector).remove() - }, - }) - } - - function setupColumnInserts(rowWrapper) { - $(rowWrapper) - .children(colWrapperClassSelector) - .each((i, elem) => { - const colWrapper = $(elem) - colWrapper.addClass('colHoverTempStyle') - - if (colWrapper.index() == 0) { - $( - ``, - ).insertBefore(colWrapper) - } - - $( - ``, - ).insertAfter(colWrapper) - }) - } + $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', + }) // ControlBox with different fields $cbUL.sortable({ - helper: function (e, el) { - //Shrink the control a little while dragging so it's not in the way as much - return el - .clone() - .css({ width: '50px', height: '35px', border: '1px', borderStyle: 'solid', borderColor: 'black' }) - .html('') - }, opacity: 0.9, - connectWith: [tmpRowWrapperClassSelector, tmpColWrapperClassSelector], + connectWith: $stage, cancel: '.formbuilder-separator', cursor: 'move', - cursorAt: { - left: 5, - top: 5, - }, scroll: false, start: (evt, ui) => { h.startMoving.call(h, evt, ui) @@ -216,7 +112,7 @@ const FormBuilder = function (opts, element, $) { stop: (evt, ui) => { h.stopMoving.call(h, evt, ui) isMoving = false - cleanupTempPlaceholders(true) + cleanupTempPlaceholders() }, revert: 150, beforeStop: (evt, ui) => { @@ -229,11 +125,18 @@ const FormBuilder = function (opts, element, $) { return false } - insertTargetIsRow = $(ui.item.parent()).hasClass(tmpRowWrapperClass) - const dropTargetIsStage = ui.item.parent().parent()[0] === d.stage + insertTargetIsStage = ui.item.parent()[0] === d.stage - if (dropTargetIsStage || insertTargetIsRow) { - insertingNewControl = true + if (insertTargetIsStage) { + if (ui.item.index() == 0) { + ui.item.attr('prepend', 'true') + } else { + ui.item.attr('appendAfter', ui.item.prev('.rowWrapper').attr('id')) + } + $targetInsertWrapper = ui.item + + h.doCancel = true + processControl(ui.item) } else { h.setFieldOrder($cbUL) h.doCancel = !opts.sortableControls @@ -296,6 +199,11 @@ const FormBuilder = function (opts, element, $) { } $(d.controls).on('click', 'li', ({ target }) => { + //Prevent duplicate add when click & dragging control to specific spot + if (isMoving) { + return + } + const $control = $(target).closest('li') h.stopIndex = undefined processControl($control) @@ -1184,30 +1092,38 @@ const FormBuilder = function (opts, element, $) { } //Add a wrapper div for the field itself. This div will be the rendered representation - const rowGroupNode2 = m('div', null, { + const colWrapperNode = m('div', null, { id: `${field.id}-cont`, className: `${columnData.columnSize} ${colWrapperClass}`, }) if (insertingNewControl && insertTargetIsColumn) { if ($targetInsertWrapper.attr('prepend') == 'true') { - $(rowGroupNode2).prependTo(rowWrapperNode) + $(colWrapperNode).prependTo(rowWrapperNode) } else { - $(rowGroupNode2).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) + $(colWrapperNode).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) } } //Control insert will take care of inserting itself if (!insertTargetIsColumn) { - $(rowGroupNode2).appendTo(rowWrapperNode) + $(colWrapperNode).appendTo(rowWrapperNode) + } + + if (insertTargetIsStage) { + if ($targetInsertWrapper.attr('prepend') == 'true') { + $(rowWrapperNode).prependTo($stage) + } else { + $(rowWrapperNode).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) + } } //If inserting, use the existing index, do not always append to end - if (!insertingNewControl) { + if (!insertingNewControl && !insertTargetIsStage) { $stage.append(rowWrapperNode) } - $li.appendTo(rowGroupNode2) + $li.appendTo(colWrapperNode) setupSortableRowWrapper(rowWrapperNode) @@ -1238,11 +1154,12 @@ const FormBuilder = function (opts, element, $) { autoSizeRowColumns(rowWrapperNode, true) } - cleanupTempPlaceholders(true) + cleanupTempPlaceholders() insertingNewControl = false insertTargetIsRow = false insertTargetIsColumn = false + insertTargetIsStage = false } function setupSortableRowWrapper(rowWrapperNode) { @@ -1252,18 +1169,20 @@ const FormBuilder = function (opts, element, $) { opacity: 0.9, revert: 150, deactivate: function () { - cleanupTempPlaceholders(true) + cleanupTempPlaceholders() }, placeholder: 'ui-state-highlight', - grid: [1, 1], receive: function (event, ui) { - cleanupTempPlaceholders(true) + cleanupTempPlaceholders() if (insertingNewControl) { h.doCancel = true processControl(ui.item) } }, + start: function () { + cleanupTempPlaceholders() + }, stop: function (event, ui) { autoSizeRowColumns(ui.item.closest(rowWrapperClassSelector)) }, @@ -1283,6 +1202,34 @@ const FormBuilder = function (opts, element, $) { }) } + function cleanupTempPlaceholders() { + $stage.find(colWrapperClassSelector).removeClass('colHoverTempStyle') + $stage.find(tmpColWrapperClassSelector).remove() + } + + function setupColumnInserts(rowWrapper) { + $(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') @@ -1903,8 +1850,13 @@ const FormBuilder = function (opts, element, $) { } }) - $(document).on('checkRowCleanup', () => { + $(document).on('checkRowCleanup', (event, data) => { checkRowCleanup() + + const rowWrapper = $(`#${data.rowWrapperID}`) + if (rowWrapper.length) { + autoSizeRowColumns(rowWrapper, true) + } }) function checkRowCleanup() { @@ -1936,7 +1888,7 @@ const FormBuilder = function (opts, element, $) { $(d.formActions).css('display', 'none') //Cleanup temp artifacts - cleanupTempPlaceholders(true) + cleanupTempPlaceholders() buildGridModeHelp() h.closeAllEdit() diff --git a/src/js/helpers.js b/src/js/helpers.js index cf08f9c27..e1590a08b 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -1027,6 +1027,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 @@ -1054,7 +1055,7 @@ export default class Helpers { this.removeContainerProtection(`${fieldID}-cont`) setTimeout(() => { - $(document).trigger('checkRowCleanup') + $(document).trigger('checkRowCleanup', [{ rowWrapperID: fieldRowWrapper.attr('id') }]) }, 300) return fieldRemoved From 0345fc5b5b9f074e5a8b9b9830a26705a6ada0b1 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 22 Jan 2022 00:22:50 -0500 Subject: [PATCH 30/56] remove old config option --- src/js/config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/config.js b/src/js/config.js index 641109342..9bf406bf8 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -86,7 +86,6 @@ export const defaultOptions = { typeUserEvents: {}, defaultGridColumnClass: 'col-md-12', cancelGridModeDistance: 100, - enableRowDrop: false, } export const styles = { From 87b3ab72a468d762a71fed33e90b60e7cad9bc02 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 22 Jan 2022 00:25:23 -0500 Subject: [PATCH 31/56] remove unused css --- src/sass/_stage.scss | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index e4d130bc5..8361677b6 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -833,28 +833,6 @@ white-space: nowrap; } -.hoverDropStyle { - border: 1px dashed #0d99f2; - border-radius: 3px; - background-color: #e5f5f8; - height: 20px; - margin: 6px; -} - -.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; -} - .colHoverTempStyle { margin-right: 10px !important; margin-left: 10px !important; From 545d25c3d43cf65cfb59646669ea996a8658cf66 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 22 Jan 2022 18:51:09 -0500 Subject: [PATCH 32/56] Allow dropping control to new row or existing row --- src/js/form-builder.js | 139 +++++++++++++++++++++++++++++++++++++++-- src/js/helpers.js | 9 ++- src/sass/_stage.scss | 28 +++++++++ 3 files changed, 167 insertions(+), 9 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 04c67c504..342ba389c 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -38,6 +38,12 @@ 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, $) { @@ -101,7 +107,7 @@ const FormBuilder = function (opts, element, $) { // ControlBox with different fields $cbUL.sortable({ opacity: 0.9, - connectWith: $stage, + connectWith: [$stage, rowWrapperClassSelector], cancel: '.formbuilder-separator', cursor: 'move', scroll: false, @@ -1127,6 +1133,8 @@ const FormBuilder = function (opts, element, $) { 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) { @@ -1162,23 +1170,135 @@ const FormBuilder = function (opts, element, $) { insertTargetIsStage = false } + function SetupInvisibleRowPlaceholders(rowWrapperNode) { + const wrapperClone = $(rowWrapperNode).clone() + wrapperClone + .addClass('hoverDropStyle') + .addClass(invisibleRowPlaceholderClass) + .addClass(tmpRowPlaceholderClass) + .html('') + wrapperClone.css('height', '50px') + + 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) { $(rowWrapperNode).sortable({ - connectWith: rowWrapperClassSelector, + connectWith: [rowWrapperClassSelector], cursor: 'move', opacity: 0.9, revert: 150, deactivate: function () { cleanupTempPlaceholders() }, - placeholder: 'ui-state-highlight', + 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%' }) + return clone + }, + over: function (event, ui) { + const overTarget = $(event.target) + const overTargetIsPlaceholder = overTarget.hasClass(tmpRowPlaceholderClass) + + if (!overTargetIsPlaceholder) { + removeColumnInsertButtons(overTarget) + } + + overTarget.addClass('hoverDropStyleInverse') + + if (!overTargetIsPlaceholder) { + $stage.children(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + + //Only show the placeholder for what is above/below the rowWrapper + overTarget.prevAll(tmpRowPlaceholderClassSelector).first().removeClass(invisibleRowPlaceholderClass) + overTarget.nextAll(tmpRowPlaceholderClassSelector).first().removeClass(invisibleRowPlaceholderClass) + } + }, + out: function (event) { + $(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) + + 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() }, start: function () { cleanupTempPlaceholders() @@ -1753,7 +1873,7 @@ const FormBuilder = function (opts, element, $) { }) function moveFieldUp(rowWrapper) { - const rowSibling = rowWrapper.prev() + const rowSibling = rowWrapper.prevAll().not(invisibleRowPlaceholderClassSelector).first() if (rowSibling.length) { gridModeTargetField.parent().appendTo(rowSibling) syncFieldWithNewRow(gridModeTargetField.attr('id')) @@ -1764,7 +1884,7 @@ const FormBuilder = function (opts, element, $) { } function moveFieldDown(rowWrapper) { - const rowSibling = rowWrapper.next() + const rowSibling = rowWrapper.nextAll().not(invisibleRowPlaceholderClassSelector).first() if (rowSibling.length) { gridModeTargetField.parent().appendTo(rowSibling) syncFieldWithNewRow(gridModeTargetField.attr('id')) @@ -1859,6 +1979,13 @@ const FormBuilder = function (opts, element, $) { } }) + $(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) @@ -1868,7 +1995,7 @@ const FormBuilder = function (opts, element, $) { }) $stage.children(rowWrapperClassSelector).each((i, elem) => { - if ($(elem).children().length == 0) { + if ($(elem).children().length == 0 && !$(elem).hasClass(invisibleRowPlaceholderClass)) { const rowValue = h.getRowValue($(elem).attr('class')) formRows = formRows.filter(x => x != rowValue) $(elem).remove() diff --git a/src/js/helpers.js b/src/js/helpers.js index e1590a08b..6ebde6753 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -896,18 +896,21 @@ export default class Helpers { this.updatePreview($(field)) const liContainer = $(`#${fieldId}`) - const rowContainer = $(`#${fieldId}-cont`) + 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(rowContainer.attr('id')) + 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(rowContainer.closest(this.formBuilder.rowWrapperClassSelector)) + 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 } diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 8361677b6..ca8a02417 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -848,3 +848,31 @@ border: 0; background-color: unset; } + +.hoverDropStyle { + border: 1px dashed #0d99f2; + border-radius: 3px; + background-color: #e5f5f8; + height: 20px; + margin: 6px; +} + +.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; +} From 4db331582e8beb5e494f09307aa97109622d7563 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 22 Jan 2022 23:20:12 -0500 Subject: [PATCH 33/56] minor cleanup --- src/js/form-builder.js | 46 ++++++++++++------------------------------ 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 342ba389c..5d559e5a2 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -79,14 +79,9 @@ const FormBuilder = function (opts, element, $) { const $stage = $(d.stage) const $cbUL = $(d.controls) - if (!opts.allowStageSort) { - $stage.sortable('disable') - } - let insertingNewControl = false let insertTargetIsRow = false let insertTargetIsColumn = false - let insertTargetIsStage = false let $targetInsertWrapper let cloneControls @@ -104,10 +99,14 @@ const FormBuilder = function (opts, element, $) { placeholder: 'frmb-placeholder', }) + if (!opts.allowStageSort) { + $stage.sortable('disable') + } + // ControlBox with different fields $cbUL.sortable({ opacity: 0.9, - connectWith: [$stage, rowWrapperClassSelector], + connectWith: rowWrapperClassSelector, cancel: '.formbuilder-separator', cursor: 'move', scroll: false, @@ -125,28 +124,18 @@ const FormBuilder = function (opts, element, $) { h.beforeStop.call(h, evt, ui) }, distance: 3, - update: function (event, ui) { + update: function (event) { isMoving = false if (h.doCancel) { return false } - insertTargetIsStage = ui.item.parent()[0] === d.stage - - if (insertTargetIsStage) { - if (ui.item.index() == 0) { - ui.item.attr('prepend', 'true') - } else { - ui.item.attr('appendAfter', ui.item.prev('.rowWrapper').attr('id')) - } - $targetInsertWrapper = ui.item - - h.doCancel = true - processControl(ui.item) - } else { - h.setFieldOrder($cbUL) - h.doCancel = !opts.sortableControls + //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')) { + $stage.children(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) } + h.setFieldOrder($cbUL) + h.doCancel = !opts.sortableControls }, }) @@ -1116,16 +1105,8 @@ const FormBuilder = function (opts, element, $) { $(colWrapperNode).appendTo(rowWrapperNode) } - if (insertTargetIsStage) { - if ($targetInsertWrapper.attr('prepend') == 'true') { - $(rowWrapperNode).prependTo($stage) - } else { - $(rowWrapperNode).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) - } - } - //If inserting, use the existing index, do not always append to end - if (!insertingNewControl && !insertTargetIsStage) { + if (!insertingNewControl) { $stage.append(rowWrapperNode) } @@ -1167,7 +1148,6 @@ const FormBuilder = function (opts, element, $) { insertingNewControl = false insertTargetIsRow = false insertTargetIsColumn = false - insertTargetIsStage = false } function SetupInvisibleRowPlaceholders(rowWrapperNode) { @@ -1216,7 +1196,7 @@ const FormBuilder = function (opts, element, $) { clone.css({ width: '20%' }) return clone }, - over: function (event, ui) { + over: function (event) { const overTarget = $(event.target) const overTargetIsPlaceholder = overTarget.hasClass(tmpRowPlaceholderClass) From aea92fba16172c7c14b3ee314658eafaafc0b89f Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 24 Jan 2022 18:07:24 -0500 Subject: [PATCH 34/56] -Improve helper size -dont allow resize mode during edit -Ensure grid mode labels work for hidden --- src/js/form-builder.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 5d559e5a2..6200843cc 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1193,7 +1193,7 @@ const FormBuilder = function (opts, element, $) { //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%' }) + clone.css({ width: '20%', height: '100px', minHeight: '60px', overflow: 'hidden' }) return clone }, over: function (event) { @@ -1763,6 +1763,10 @@ const FormBuilder = function (opts, element, $) { //Use E to enter into Grid Mode for the currently active(hovered field) $(document).keydown(e => { + if (gridModeTargetField && gridModeTargetField.hasClass('editing')) { + return + } + if (e.keyCode == 69 && gridModeTargetField) { e.preventDefault() toggleGridModeActive() @@ -2105,7 +2109,16 @@ const FormBuilder = function (opts, element, $) { rowWrapper.children(`div${colWrapperClassSelector}`).each((i, elem) => { const colWrapper = $(`#${elem.id}`) const fieldID = colWrapper.find('li').attr('id') - const label = $(`#label-${fieldID}`).html() + 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 = '' From daae31c4347dfee6313d391fd512bf48bd8d44a6 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 25 Jan 2022 17:40:27 -0500 Subject: [PATCH 35/56] Dont allow pressing E to enter grid mode while editing the preview input field --- src/js/form-builder.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 6200843cc..9554c6e88 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1767,6 +1767,15 @@ const FormBuilder = function (opts, element, $) { return } + //Don't enter grid mode if you are on an input field of the element just typing + if ( + gridModeTargetField && + $(document.activeElement).is('input') && + $(document.activeElement).closest('li').attr('id') == gridModeTargetField.attr('id') + ) { + return + } + if (e.keyCode == 69 && gridModeTargetField) { e.preventDefault() toggleGridModeActive() From 643dc1af78395511a59cf6d7701574aa585d2e57 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 25 Jan 2022 17:41:15 -0500 Subject: [PATCH 36/56] -Tinymce js file is no longer being hosted -Tinymce needs a small timeout to load. --- src/js/control/textarea.tinymce.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/js/control/textarea.tinymce.js b/src/js/control/textarea.tinymce.js index 210fcdd42..d6dcdfc7e 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 + * } * } * }; * ``` @@ -21,7 +21,7 @@ export default class controlTinymce extends controlTextarea { * configure the tinymce editor requirements */ configure() { - this.js = ['https://cdn.tinymce.com/4/tinymce.min.js'] + this.js = ['https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js'] // additional javascript config if (this.classConfig.js) { @@ -78,8 +78,11 @@ 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) { From 4e835a52ac110fada500e2828096db06b080a3a4 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 25 Jan 2022 23:31:52 -0500 Subject: [PATCH 37/56] Put js file change on another PR --- src/js/control/textarea.tinymce.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/control/textarea.tinymce.js b/src/js/control/textarea.tinymce.js index d6dcdfc7e..9ddc65ffc 100644 --- a/src/js/control/textarea.tinymce.js +++ b/src/js/control/textarea.tinymce.js @@ -21,7 +21,7 @@ export default class controlTinymce extends controlTextarea { * configure the tinymce editor requirements */ configure() { - this.js = ['https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js'] + this.js = ['https://cdn.tinymce.com/4/tinymce.min.js'] // additional javascript config if (this.classConfig.js) { From 83ba1437f52277a48c0b53d895985270372e6df2 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Wed, 26 Jan 2022 17:08:54 -0500 Subject: [PATCH 38/56] -Ensure cloned controls are always visible - Remove form-inline as it causes rendering for checkboxes smaller than 12 to render weird --- src/js/form-builder.js | 7 +++++++ src/js/form-render.js | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 9554c6e88..df0c10345 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1670,6 +1670,13 @@ const FormBuilder = function (opts, element, $) { 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 diff --git a/src/js/form-render.js b/src/js/form-render.js index 49d86527b..aa9a436b3 100644 --- a/src/js/form-render.js +++ b/src/js/form-render.js @@ -81,7 +81,7 @@ class FormRender { * @param {Array} fields array of elements */ if (typeof Element.prototype.appendFormFields !== 'function') { - Element.prototype.appendFormFields = function(fields) { + Element.prototype.appendFormFields = function (fields) { if (!Array.isArray(fields)) { fields = [fields] } @@ -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) @@ -118,7 +118,7 @@ class FormRender { * Extend Element prototype to remove content */ if (typeof Element.prototype.emptyContainer !== 'function') { - Element.prototype.emptyContainer = function() { + Element.prototype.emptyContainer = function () { const element = this while (element.lastChild) { element.removeChild(element.lastChild) @@ -175,7 +175,7 @@ class FormRender { const opts = this.options element = this.getElement(element) - const runCallbacks = function() { + const runCallbacks = function () { if (opts.onRender) { opts.onRender() } @@ -228,8 +228,8 @@ class FormRender { } if (opts.disableInjectedStyle) { - const styleTags = document.getElementsByClassName('formBuilder-injected-style') - forEach(styleTags, i => remove(styleTags[i])) + const styleTags = document.getElementsByClassName('formBuilder-injected-style') + forEach(styleTags, i => remove(styleTags[i])) } return formRender } @@ -245,7 +245,7 @@ class FormRender { const fieldData = opts.formData if (!fieldData || Array.isArray(fieldData)) { throw new Error( - 'To render a single element, please specify a single object of formData for the field in question' + 'To render a single element, please specify a single object of formData for the field in question', ) } const sanitizedField = this.santizeField(fieldData) @@ -335,7 +335,7 @@ class FormRender { } } -(function() { +;(function () { let formRenderForms const methods = { init: (forms, options = {}) => { @@ -362,7 +362,7 @@ class FormRender { html: () => formRenderForms.map(index => formRenderForms[index]).html(), } - jQuery.fn.formRender = function(methodOrOptions = {}, ...args) { + jQuery.fn.formRender = function (methodOrOptions = {}, ...args) { if (methods[methodOrOptions]) { return methods[methodOrOptions].apply(this, args) } else { @@ -378,7 +378,7 @@ class FormRender { * @param {Object} options - optional subset of formRender options - doesn't support container or other form rendering based options. * @return {DOMElement} the rendered field */ - jQuery.fn.controlRender = function(data, options = {}) { + jQuery.fn.controlRender = function (data, options = {}) { options.formData = data options.dataType = typeof data === 'string' ? 'json' : 'xml' const formRender = new FormRender(options) From 9e995144c2657c4d04aaf2f2fcdb599717bfb0fe Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Wed, 26 Jan 2022 17:11:26 -0500 Subject: [PATCH 39/56] - Fix subtype copy - Attach handlers when dropping to new row - Adjust css for column insert buttons --- src/js/form-builder.js | 43 +++++++++++++++++++++++++++++++++--------- src/sass/_stage.scss | 6 +++--- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index df0c10345..b887708d7 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1041,15 +1041,7 @@ const FormBuilder = function (opts, element, $) { }) const $li = $(field) - $li - .mouseenter(function (e) { - if (!gridMode) { - gridModeTargetField = $(this) - gridModeStartX = e.pageX - gridModeStartY = e.pageY - } - }) - .mouseleave(function () {}) + AttatchColWrapperHandler($li) $li.data('fieldData', { attrs: values }) @@ -1150,6 +1142,30 @@ const FormBuilder = function (opts, element, $) { insertTargetIsColumn = false } + function AttatchColWrapperHandler(colWrapper) { + colWrapper.mouseenter(function (e) { + $stage.find(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + + //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) + + if (!gridMode) { + gridModeTargetField = $(this) + gridModeStartX = e.pageX + gridModeStartY = e.pageY + } + }) + } + function SetupInvisibleRowPlaceholders(rowWrapperNode) { const wrapperClone = $(rowWrapperNode).clone() wrapperClone @@ -1188,6 +1204,7 @@ const FormBuilder = function (opts, element, $) { revert: 150, deactivate: function () { cleanupTempPlaceholders() + ResetAllInvisibleRowPlaceholders() }, helper: function (e, el) { //Shrink the control a little while dragging so it's not in the way as much @@ -1237,6 +1254,7 @@ const FormBuilder = function (opts, element, $) { }) $(ui.item).parent().replaceWith(rowWrapperNode) + AttatchColWrapperHandler($(ui.item)) colWrapper.appendTo(rowWrapperNode) @@ -1440,6 +1458,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') diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index ca8a02417..0973d6f13 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -834,9 +834,9 @@ } .colHoverTempStyle { - margin-right: 10px !important; - margin-left: 10px !important; - flex: 95 !important; + padding-left: 7px !important; + padding-right: 7px !important; + flex: 95 1 0% !important; } .rowWrapper { From 36325875497bbaba339bc19ae5a8948414ae5b58 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 27 Jan 2022 00:15:56 -0500 Subject: [PATCH 40/56] Improve grid mode to allow moving entire row up/down. --- src/js/form-builder.js | 91 +++++++++++++++++------------------------- src/js/utils.js | 15 +++++++ 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index b887708d7..250a75cd8 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -132,7 +132,7 @@ const FormBuilder = function (opts, element, $) { //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')) { - $stage.children(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + HideInvisibleRowPlaceholders() } h.setFieldOrder($cbUL) h.doCancel = !opts.sortableControls @@ -1144,21 +1144,21 @@ const FormBuilder = function (opts, element, $) { function AttatchColWrapperHandler(colWrapper) { colWrapper.mouseenter(function (e) { - $stage.find(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) - - //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) - 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 @@ -1166,6 +1166,10 @@ const FormBuilder = function (opts, element, $) { }) } + function HideInvisibleRowPlaceholders() { + $stage.find(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + } + function SetupInvisibleRowPlaceholders(rowWrapperNode) { const wrapperClone = $(rowWrapperNode).clone() wrapperClone @@ -1173,7 +1177,7 @@ const FormBuilder = function (opts, element, $) { .addClass(invisibleRowPlaceholderClass) .addClass(tmpRowPlaceholderClass) .html('') - wrapperClone.css('height', '50px') + wrapperClone.css('height', '40px') wrapperClone.attr('class', wrapperClone.attr('class').replace('row-', '')) wrapperClone.removeAttr('id') @@ -1224,7 +1228,7 @@ const FormBuilder = function (opts, element, $) { overTarget.addClass('hoverDropStyleInverse') if (!overTargetIsPlaceholder) { - $stage.children(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + HideInvisibleRowPlaceholders() //Only show the placeholder for what is above/below the rowWrapper overTarget.prevAll(tmpRowPlaceholderClassSelector).first().removeClass(invisibleRowPlaceholderClass) @@ -1898,23 +1902,21 @@ const FormBuilder = function (opts, element, $) { }) function moveFieldUp(rowWrapper) { - const rowSibling = rowWrapper.prevAll().not(invisibleRowPlaceholderClassSelector).first() - if (rowSibling.length) { - gridModeTargetField.parent().appendTo(rowSibling) - syncFieldWithNewRow(gridModeTargetField.attr('id')) + const previousRowSibling = rowWrapper.prevAll().not(tmpRowPlaceholderClassSelector).first() + if (previousRowSibling.length) { + $(gridModeTargetField.parent().parent()).swapWith(previousRowSibling) } else { - createNewRow(true) + return } h.toggleHighlight(gridModeTargetField) } function moveFieldDown(rowWrapper) { - const rowSibling = rowWrapper.nextAll().not(invisibleRowPlaceholderClassSelector).first() - if (rowSibling.length) { - gridModeTargetField.parent().appendTo(rowSibling) - syncFieldWithNewRow(gridModeTargetField.attr('id')) + const nextRowSibling = rowWrapper.nextAll().not(invisibleRowPlaceholderClassSelector).first() + if (nextRowSibling.length) { + $(gridModeTargetField.parent().parent()).swapWith(nextRowSibling) } else { - createNewRow() + return } h.toggleHighlight(gridModeTargetField) } @@ -1934,28 +1936,6 @@ const FormBuilder = function (opts, element, $) { } h.toggleHighlight(gridModeTargetField) } - - function createNewRow(prepend = false) { - const columnData = prepareFieldRow({}) - - const rowWrapperNode = m('div', null, { - id: `${gridModeTargetField.attr('id')}-row`, - className: `row row-${columnData.rowNumber} ${rowWrapperClass}`, - }) - - gridModeTargetField.parent().appendTo(rowWrapperNode) - - if (prepend) { - $stage.prepend(rowWrapperNode) - } else { - $stage.append(rowWrapperNode) - } - - setupSortableRowWrapper(rowWrapperNode) - syncFieldWithNewRow(gridModeTargetField.attr('id')) - checkRowCleanup() - } - function autoSizeRowColumns(rowWrapper, force = false) { const childRowCount = rowWrapper.children(`div${colWrapperClassSelector}`).length const newAutoCalcSizeValue = Math.floor(12 / childRowCount) @@ -2045,6 +2025,7 @@ const FormBuilder = function (opts, element, $) { buildGridModeHelp() h.closeAllEdit() h.toggleHighlight(gridModeTargetField) + HideInvisibleRowPlaceholders() } else { h.showToast('Grid Mode Finished', 1500) @@ -2078,7 +2059,7 @@ const FormBuilder = function (opts, element, $) {

Grid Mode

- +
@@ -2092,19 +2073,19 @@ const FormBuilder = function (opts, element, $) { - + - + - - + + - + diff --git a/src/js/utils.js b/src/js/utils.js index 4317ffbf4..582b2a935 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -736,4 +736,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 From c4fac693d12a8501bbc222a11151efdb36a04f02 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 27 Jan 2022 01:33:44 -0500 Subject: [PATCH 41/56] Multiple fixes to existing TinyMCE issues - Copying tinymce now copies data - Moving tinymce renders correctly - Moving tinymce keeps data - Fix link to no longer hosted tinymce js file --- src/js/control/textarea.tinymce.js | 10 +++++++++- src/js/form-builder.js | 32 ++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/js/control/textarea.tinymce.js b/src/js/control/textarea.tinymce.js index 9ddc65ffc..f40b24ae3 100644 --- a/src/js/control/textarea.tinymce.js +++ b/src/js/control/textarea.tinymce.js @@ -21,7 +21,7 @@ export default class controlTinymce extends controlTextarea { * configure the tinymce editor requirements */ configure() { - this.js = ['https://cdn.tinymce.com/4/tinymce.min.js'] + this.js = ['https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js'] // additional javascript config if (this.classConfig.js) { @@ -88,6 +88,14 @@ export default class controlTinymce extends controlTextarea { if (this.config.userData) { window.tinymce.editors[this.id].setContent(this.parsedHtml(this.config.userData[0])) } + + if (window.lastFormBuilderCopiedTinyMCE) { + setTimeout(() => { + window.tinymce.editors[this.id].setContent(this.parsedHtml(window.lastFormBuilderCopiedTinyMCE)) + window.lastFormBuilderCopiedTinyMCE = null + }, 300) + } + return evt } } diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 250a75cd8..d4ac1a57f 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1301,6 +1301,11 @@ const FormBuilder = function (opts, element, $) { } ResetAllInvisibleRowPlaceholders() + + const listFieldItem = $(ui.item).find('li') + + CheckTinyMCETransition(listFieldItem) + UpdatePreviewAndSave(listFieldItem) }, start: function () { cleanupTempPlaceholders() @@ -1324,6 +1329,18 @@ const FormBuilder = function (opts, element, $) { }) } + 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() @@ -1446,6 +1463,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() @@ -1492,8 +1512,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')) } } @@ -1516,8 +1535,7 @@ const FormBuilder = function (opts, element, $) { } else { $option.slideUp('250', () => { $option.remove() - h.updatePreview($field) - h.save.call(h) + UpdatePreviewAndSave($field) }) } }) @@ -1714,8 +1732,7 @@ const FormBuilder = function (opts, element, $) { const currentItem = $(evt.target).parent().parent('li') const $clone = cloneItem(currentItem) prepareCloneWrappers($clone, currentItem) - h.updatePreview($clone) - h.save.call(h) + UpdatePreviewAndSave($clone) h.tmpCleanPrevHolder($clone.find('.prev-holder')) }) @@ -2168,8 +2185,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 From abfaeb225ca1de04944ec51f8392e7fed5d9d4d7 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 27 Jan 2022 12:53:13 -0500 Subject: [PATCH 42/56] Need to preserve added row and col information due to the fact the user may be saving the config and reloading afterward. --- src/js/form-builder.js | 7 ------- src/js/helpers.js | 16 ---------------- 2 files changed, 23 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index d4ac1a57f..cc67d2202 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1108,12 +1108,6 @@ const FormBuilder = function (opts, element, $) { 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) { @@ -1410,7 +1404,6 @@ const FormBuilder = function (opts, element, $) { } data.className += ` row-${result.rowNumber} ${result.columnSize}` - result.addedDefaultColumnClass = true } } } diff --git a/src/js/helpers.js b/src/js/helpers.js index 6ebde6753..b27b77dea 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -249,22 +249,6 @@ export default class Helpers { 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) { From 0c3f97b369e344167a2d69172c6337fc1e1cb08d Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 27 Jan 2022 17:08:00 -0500 Subject: [PATCH 43/56] When stage is empty, allow dragging for 1st field --- src/js/form-builder.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index cc67d2202..4a84519e7 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1297,9 +1297,10 @@ const FormBuilder = function (opts, element, $) { ResetAllInvisibleRowPlaceholders() const listFieldItem = $(ui.item).find('li') - - CheckTinyMCETransition(listFieldItem) - UpdatePreviewAndSave(listFieldItem) + if (listFieldItem.length) { + CheckTinyMCETransition(listFieldItem) + UpdatePreviewAndSave(listFieldItem) + } }, start: function () { cleanupTempPlaceholders() @@ -1992,6 +1993,8 @@ const FormBuilder = function (opts, element, $) { if (rowWrapper.length) { autoSizeRowColumns(rowWrapper, true) } + + checkSetupBlankStage() }) $(document).on('fieldOpened', (event, data) => { @@ -2020,6 +2023,24 @@ const FormBuilder = function (opts, element, $) { }) } + function checkSetupBlankStage() { + if ($stage.find('li').length > 0) { + 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() + $stage.find(tmpRowPlaceholderClassSelector).eq(0).removeClass(invisibleRowPlaceholderClass) + } + function toggleGridModeActive(active = true) { if (active) { gridMode = true @@ -2300,6 +2321,9 @@ const FormBuilder = function (opts, element, $) { if (opts.stickyControls.enable) { h.stickyControls($stage) } + + checkSetupBlankStage() + clearTimeout(onRenderTimeout) }, 0) }) From d7e1d338b347d2793403e4e20fca554ab6882477 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 27 Jan 2022 17:46:43 -0500 Subject: [PATCH 44/56] Fix performance issue with large forms --- src/js/form-builder.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 4a84519e7..502adebad 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1200,10 +1200,6 @@ const FormBuilder = function (opts, element, $) { cursor: 'move', opacity: 0.9, revert: 150, - deactivate: function () { - cleanupTempPlaceholders() - ResetAllInvisibleRowPlaceholders() - }, helper: function (e, el) { //Shrink the control a little while dragging so it's not in the way as much const clone = el.clone() @@ -1306,6 +1302,7 @@ const FormBuilder = function (opts, element, $) { cleanupTempPlaceholders() }, stop: function (event, ui) { + $stage.children(tmpRowPlaceholderClassSelector).removeClass('hoverDropStyleInverse') autoSizeRowColumns(ui.item.closest(rowWrapperClassSelector)) }, update: function (event, ui) { From 3330f16acddc5588c99cc7ab5d11f0ca4094bb2b Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 28 Jan 2022 02:17:01 -0500 Subject: [PATCH 45/56] - Remove blue placeholders showing up even on mousehover, Only show visible when dropping - Make initial placeholder for blank stage fill the entire height so drag can happen anywhere --- src/js/form-builder.js | 32 +++++++++++++++++++++++++------- src/sass/_stage.scss | 8 -------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 502adebad..d3820cc2a 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -139,6 +139,12 @@ const FormBuilder = function (opts, element, $) { }, }) + $cbUL.on('mouseenter', function (e) { + if (stageHasFields()) { + $stage.children(tmpRowPlaceholderClassSelector).addClass(invisibleRowPlaceholderClass) + } + }) + const processControl = control => { if (control[0].classList.contains('input-set-control')) { const inputSets = [] @@ -199,6 +205,11 @@ const FormBuilder = function (opts, element, $) { 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) @@ -1166,11 +1177,7 @@ const FormBuilder = function (opts, element, $) { function SetupInvisibleRowPlaceholders(rowWrapperNode) { const wrapperClone = $(rowWrapperNode).clone() - wrapperClone - .addClass('hoverDropStyle') - .addClass(invisibleRowPlaceholderClass) - .addClass(tmpRowPlaceholderClass) - .html('') + wrapperClone.addClass(invisibleRowPlaceholderClass).addClass(tmpRowPlaceholderClass).html('') wrapperClone.css('height', '40px') wrapperClone.attr('class', wrapperClone.attr('class').replace('row-', '')) @@ -1226,6 +1233,7 @@ const FormBuilder = function (opts, element, $) { } }, out: function (event) { + $stage.children(tmpRowPlaceholderClassSelector).removeClass('hoverDropStyleInverse') $(event.target).removeClass('hoverDropStyleInverse') }, placeholder: 'hoverDropStyleInverse', @@ -2020,8 +2028,12 @@ const FormBuilder = function (opts, element, $) { }) } + function stageHasFields() { + return $stage.find('li').length > 0 + } + function checkSetupBlankStage() { - if ($stage.find('li').length > 0) { + if (stageHasFields()) { return } @@ -2035,7 +2047,13 @@ const FormBuilder = function (opts, element, $) { $stage.append(rowWrapperNode) setupSortableRowWrapper(rowWrapperNode) ResetAllInvisibleRowPlaceholders() - $stage.find(tmpRowPlaceholderClassSelector).eq(0).removeClass(invisibleRowPlaceholderClass) + + //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) { diff --git a/src/sass/_stage.scss b/src/sass/_stage.scss index 0973d6f13..70fc1786a 100644 --- a/src/sass/_stage.scss +++ b/src/sass/_stage.scss @@ -849,14 +849,6 @@ background-color: unset; } -.hoverDropStyle { - border: 1px dashed #0d99f2; - border-radius: 3px; - background-color: #e5f5f8; - height: 20px; - margin: 6px; -} - .hoverColumnDropStyle { border: 1px dashed #0d99f2; border-radius: 3px; From 2bd317e9288823f4c602b0ac314e72407bcb5ef6 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 28 Jan 2022 02:53:25 -0500 Subject: [PATCH 46/56] Ensure row is deleted when no more cols exist --- src/js/form-builder.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index d3820cc2a..d53ffeb76 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -2017,15 +2017,18 @@ const FormBuilder = function (opts, element, $) { } }) - $stage.children(rowWrapperClassSelector).each((i, elem) => { - if ($(elem).children().length == 0 && !$(elem).hasClass(invisibleRowPlaceholderClass)) { - const rowValue = h.getRowValue($(elem).attr('class')) - formRows = formRows.filter(x => x != rowValue) - $(elem).remove() - } else { - removeColumnInsertButtons($(elem)) - } - }) + $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() { From dba225e2b09276ffea562501e803b156badc2189 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Fri, 28 Jan 2022 22:03:32 -0500 Subject: [PATCH 47/56] -Make temp row even more less noticable. -Tighten drop precision --- src/js/form-builder.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index d53ffeb76..b48345552 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1178,7 +1178,7 @@ const FormBuilder = function (opts, element, $) { function SetupInvisibleRowPlaceholders(rowWrapperNode) { const wrapperClone = $(rowWrapperNode).clone() wrapperClone.addClass(invisibleRowPlaceholderClass).addClass(tmpRowPlaceholderClass).html('') - wrapperClone.css('height', '40px') + wrapperClone.css('height', '1px') wrapperClone.attr('class', wrapperClone.attr('class').replace('row-', '')) wrapperClone.removeAttr('id') @@ -1207,6 +1207,7 @@ const FormBuilder = function (opts, element, $) { 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() @@ -1228,8 +1229,16 @@ const FormBuilder = function (opts, element, $) { HideInvisibleRowPlaceholders() //Only show the placeholder for what is above/below the rowWrapper - overTarget.prevAll(tmpRowPlaceholderClassSelector).first().removeClass(invisibleRowPlaceholderClass) - overTarget.nextAll(tmpRowPlaceholderClassSelector).first().removeClass(invisibleRowPlaceholderClass) + overTarget + .prevAll(tmpRowPlaceholderClassSelector) + .first() + .removeClass(invisibleRowPlaceholderClass) + .css('height', '40px') + overTarget + .nextAll(tmpRowPlaceholderClassSelector) + .first() + .removeClass(invisibleRowPlaceholderClass) + .css('height', '40px') } }, out: function (event) { From 4fd00733194cdb17587b005de8eacf2834c063b7 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 29 Jan 2022 01:15:22 -0500 Subject: [PATCH 48/56] -Reintroduce the concept of removing default information -Prevent previous form issues by defaulting default row to a number that was likely never used --- src/js/config.js | 1 + src/js/form-builder.js | 19 ++++++++++--------- src/js/helpers.js | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index 9bf406bf8..962a3de1b 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -85,6 +85,7 @@ export const defaultOptions = { typeUserDisabledAttrs: {}, typeUserEvents: {}, defaultGridColumnClass: 'col-md-12', + defaultGridStartingRow: 1000, cancelGridModeDistance: 100, } diff --git a/src/js/form-builder.js b/src/js/form-builder.js index b48345552..7f3d16560 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1119,6 +1119,12 @@ const FormBuilder = function (opts, element, $) { 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) { @@ -1397,15 +1403,9 @@ const FormBuilder = function (opts, element, $) { 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 + //Column information wasn't defined, get new default configuration for one. + result.rowNumber = opts.defaultGridStartingRow + opts.defaultGridStartingRow += 1 //If inserting directly into column, use the correct rowNumber if (insertingNewControl && insertTargetIsColumn) { @@ -1419,6 +1419,7 @@ const FormBuilder = function (opts, element, $) { } data.className += ` row-${result.rowNumber} ${result.columnSize}` + result.addedDefaultColumnClass = true } } } diff --git a/src/js/helpers.js b/src/js/helpers.js index b27b77dea..6ebde6753 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -249,6 +249,22 @@ export default class Helpers { 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) { From 8300864d09238c6c9642f5735c80b7797931ef94 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 29 Jan 2022 01:20:32 -0500 Subject: [PATCH 49/56] Add configuration option for column insert buttons --- src/js/config.js | 1 + src/js/form-builder.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/js/config.js b/src/js/config.js index 962a3de1b..fc76edc36 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -87,6 +87,7 @@ export const defaultOptions = { defaultGridColumnClass: 'col-md-12', defaultGridStartingRow: 1000, cancelGridModeDistance: 100, + enableColumnInsertMenu: true, } export const styles = { diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 7f3d16560..e990a1ff3 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1362,6 +1362,10 @@ const FormBuilder = function (opts, element, $) { } function setupColumnInserts(rowWrapper) { + if (!opts.enableColumnInsertMenu) { + return + } + $(rowWrapper) .children(colWrapperClassSelector) .each((i, elem) => { From 999af354d52dcb65e5d1995e3fbb60d6030ed934 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Sat, 29 Jan 2022 19:14:36 -0500 Subject: [PATCH 50/56] Fix row regen --- src/js/config.js | 1 - src/js/form-builder.js | 24 ++++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index fc76edc36..381097a4d 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -85,7 +85,6 @@ export const defaultOptions = { typeUserDisabledAttrs: {}, typeUserEvents: {}, defaultGridColumnClass: 'col-md-12', - defaultGridStartingRow: 1000, cancelGridModeDistance: 100, enableColumnInsertMenu: true, } diff --git a/src/js/form-builder.js b/src/js/form-builder.js index e990a1ff3..e445daf72 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -309,9 +309,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') @@ -325,6 +331,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] * @@ -1408,8 +1422,14 @@ const FormBuilder = function (opts, element, $) { function TryCreateNew() { if (!result.rowNumber) { //Column information wasn't defined, get new default configuration for one. - result.rowNumber = opts.defaultGridStartingRow - opts.defaultGridStartingRow += 1 + 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) { From 8854052e8132c6d225d054287428942f784763c3 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Tue, 1 Feb 2022 18:28:45 -0500 Subject: [PATCH 51/56] Remove keycode E for entering grid mode. Too much hassle --- src/js/form-builder.js | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index e445daf72..420920981 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1847,27 +1847,6 @@ const FormBuilder = function (opts, element, $) { toggleGridModeActive() }) - //Use E to enter into Grid Mode for the currently active(hovered field) - $(document).keydown(e => { - if (gridModeTargetField && gridModeTargetField.hasClass('editing')) { - return - } - - //Don't enter grid mode if you are on an input field of the element just typing - if ( - gridModeTargetField && - $(document.activeElement).is('input') && - $(document.activeElement).closest('li').attr('id') == gridModeTargetField.attr('id') - ) { - return - } - - if (e.keyCode == 69 && gridModeTargetField) { - e.preventDefault() - toggleGridModeActive() - } - }) - //Use mousewheel to work resizing $stage.bind('mousewheel', function (e) { if (gridMode) { @@ -2175,9 +2154,6 @@ const FormBuilder = function (opts, element, $) {
- - -
Action
W or ↑Move the field up/into another rowMove entire row up
S or ↓Move the field down/into another rowMove entire row down
A or ←Move the field left within the rowA or ←Move field left within the row
D or →Move the field right within the rowMove field right within the row
RResize all fields within the row to be maximally equal
EEnter Grid Mode when hovering over a form field
From faf7a2f3fcbad5f8b26e46094ff130916fdd0875 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 3 Feb 2022 12:24:24 -0500 Subject: [PATCH 52/56] Adjust copy field location --- src/js/form-builder.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 420920981..229041b0f 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1768,6 +1768,10 @@ const FormBuilder = function (opts, element, $) { UpdatePreviewAndSave($clone) h.tmpCleanPrevHolder($clone.find('.prev-holder')) + + if (opts.editOnAdd) { + h.closeField(data.lastID, false) + } }) function prepareCloneWrappers($clone, currentItem) { @@ -1789,7 +1793,7 @@ const FormBuilder = function (opts, element, $) { if (currentItem.parent().is('div')) { insertAfterElement = currentItem.closest(rowWrapperClassSelector) } else if (currentItem.parent().is('ul')) { - insertAfterElement = currentItem.prev(rowWrapperClassSelector) + insertAfterElement = currentItem } $(rowWrapper).insertAfter(insertAfterElement) From e6621af5b2aa29ace4241df80dcbc85ee6010159 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Thu, 3 Feb 2022 20:06:30 -0500 Subject: [PATCH 53/56] Always autosize when dropping --- src/js/form-builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/form-builder.js b/src/js/form-builder.js index 229041b0f..f91ecac5e 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -1340,7 +1340,7 @@ const FormBuilder = function (opts, element, $) { }, stop: function (event, ui) { $stage.children(tmpRowPlaceholderClassSelector).removeClass('hoverDropStyleInverse') - autoSizeRowColumns(ui.item.closest(rowWrapperClassSelector)) + autoSizeRowColumns(ui.item.closest(rowWrapperClassSelector), true) }, update: function (event, ui) { syncFieldWithNewRow(ui.item.attr('id')) From c564e3515f0b6fdb7b65be1e334140620d2cc71d Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Wed, 9 Feb 2022 12:39:01 -0500 Subject: [PATCH 54/56] close edit on save --- src/js/helpers.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/helpers.js b/src/js/helpers.js index 6ebde6753..de70b87f3 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -1263,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), From b4851aa38fa9a92bb83cb71255662f86f456daaf Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 7 Mar 2022 23:44:10 -0500 Subject: [PATCH 55/56] Implement enableEnhancedBootstrapGrid option --- src/js/config.js | 3 +- src/js/form-builder.js | 250 ++++++++++++++++++++++++++--------------- 2 files changed, 159 insertions(+), 94 deletions(-) diff --git a/src/js/config.js b/src/js/config.js index 381097a4d..02634ab5f 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -86,7 +86,8 @@ export const defaultOptions = { typeUserEvents: {}, defaultGridColumnClass: 'col-md-12', cancelGridModeDistance: 100, - enableColumnInsertMenu: true, + enableColumnInsertMenu: false, + enableEnhancedBootstrapGrid: false, } export const styles = { diff --git a/src/js/form-builder.js b/src/js/form-builder.js index f91ecac5e..689c0f716 100644 --- a/src/js/form-builder.js +++ b/src/js/form-builder.js @@ -86,6 +86,14 @@ const FormBuilder = function (opts, element, $) { let $targetInsertWrapper let cloneControls + function enhancedBootstrapEnabled() { + if (!opts.enableEnhancedBootstrapGrid) { + return false + } + + return true + } + $stage.sortable({ cursor: 'move', opacity: 0.9, @@ -103,43 +111,73 @@ const FormBuilder = function (opts, element, $) { $stage.sortable('disable') } - // 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 (!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 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 - }, - }) + 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 + } - $cbUL.on('mouseenter', function (e) { + //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) } @@ -1007,14 +1045,19 @@ const FormBuilder = function (opts, element, $) { className: `copy-button btn ${css_prefix_text}copy`, title: mi18n.get('copyButtonTooltip'), }), - m('a', null, { - type: 'grid', - id: data.lastID + '-grid', - className: `grid-button btn ${css_prefix_text}grid`, - title: 'Grid Mode', - }), ] + 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)) } @@ -1081,65 +1124,67 @@ const FormBuilder = function (opts, element, $) { // generate the control, insert it into the list item & add it to the stage h.updatePreview($li) - const targetRow = `div.row-${columnData.rowNumber}` - let rowWrapperNode - //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}`, - }) - } + if (enhancedBootstrapEnabled()) { + const targetRow = `div.row-${columnData.rowNumber}` - //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 - } + //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}`, + }) + } - //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}`, - }) + //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 + } - if (insertingNewControl && insertTargetIsColumn) { - if ($targetInsertWrapper.attr('prepend') == 'true') { - $(colWrapperNode).prependTo(rowWrapperNode) - } else { - $(colWrapperNode).insertAfter(`#${$targetInsertWrapper.attr('appendAfter')}`) + //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) - } + //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) - } + //If inserting, use the existing index, do not always append to end + if (!insertingNewControl) { + $stage.append(rowWrapperNode) + } - $li.appendTo(colWrapperNode) + $li.appendTo(colWrapperNode) - setupSortableRowWrapper(rowWrapperNode) + setupSortableRowWrapper(rowWrapperNode) - SetupInvisibleRowPlaceholders(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) - } + //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)) + h.tmpCleanPrevHolder($(prevHolder)) + } if (opts.typeUserEvents[type] && opts.typeUserEvents[type].onadd) { opts.typeUserEvents[type].onadd(field) @@ -1155,12 +1200,14 @@ const FormBuilder = function (opts, element, $) { } } - //Autosize entire row when using new insert mode - if (insertingNewControl && insertTargetIsColumn) { - autoSizeRowColumns(rowWrapperNode, true) - } + if (enhancedBootstrapEnabled()) { + //Autosize entire row when using new insert mode + if (insertingNewControl && insertTargetIsColumn) { + autoSizeRowColumns(rowWrapperNode, true) + } - cleanupTempPlaceholders() + cleanupTempPlaceholders() + } insertingNewControl = false insertTargetIsRow = false @@ -1168,6 +1215,10 @@ const FormBuilder = function (opts, element, $) { } function AttatchColWrapperHandler(colWrapper) { + if (!enhancedBootstrapEnabled()) { + return + } + colWrapper.mouseenter(function (e) { if (!gridMode) { HideInvisibleRowPlaceholders() @@ -1222,6 +1273,10 @@ const FormBuilder = function (opts, element, $) { } function setupSortableRowWrapper(rowWrapperNode) { + if (!enhancedBootstrapEnabled()) { + return + } + $(rowWrapperNode).sortable({ connectWith: [rowWrapperClassSelector], cursor: 'move', @@ -1410,6 +1465,10 @@ const FormBuilder = function (opts, element, $) { function prepareFieldRow(data) { let result = {} + if (!enhancedBootstrapEnabled()) { + return result + } + result = h.tryParseColumnInfo(data) TryCreateNew() @@ -1775,6 +1834,11 @@ const FormBuilder = function (opts, element, $) { }) function prepareCloneWrappers($clone, currentItem) { + if (!enhancedBootstrapEnabled()) { + $clone.insertAfter(currentItem) + return + } + const inputClassElement = $(`#className-${currentItem.attr('id')}`) const columnData = prepareFieldRow({}) @@ -2053,7 +2117,7 @@ const FormBuilder = function (opts, element, $) { } function checkSetupBlankStage() { - if (stageHasFields()) { + if (stageHasFields() || !enhancedBootstrapEnabled()) { return } From d7f187983ff3e03ee5e79e8a55b7924cf318dbc1 Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 7 Mar 2022 23:45:36 -0500 Subject: [PATCH 56/56] Update src/js/control/textarea.tinymce.js Co-authored-by: Kevin Chappell --- src/js/control/textarea.tinymce.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/control/textarea.tinymce.js b/src/js/control/textarea.tinymce.js index f40b24ae3..4ac1b1f9a 100644 --- a/src/js/control/textarea.tinymce.js +++ b/src/js/control/textarea.tinymce.js @@ -90,9 +90,10 @@ export default class controlTinymce extends controlTextarea { } if (window.lastFormBuilderCopiedTinyMCE) { - setTimeout(() => { + const timeout = setTimeout(() => { window.tinymce.editors[this.id].setContent(this.parsedHtml(window.lastFormBuilderCopiedTinyMCE)) window.lastFormBuilderCopiedTinyMCE = null + clearTimeout(timeout) }, 300) }