From fd27c65e7e0ec12d247ce1dbd70794d22d5fad8a Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Tue, 28 May 2019 23:52:17 -0700 Subject: [PATCH] fix: field ids for input groups --- src/js/common/dom.js | 1 - src/js/components/component.js | 2 +- src/js/constants.js | 2 + src/js/renderer.js | 235 ++++++++++++++++----------------- src/sass/_render.scss | 43 ++++-- src/sass/base/_variables.scss | 14 +- 6 files changed, 153 insertions(+), 144 deletions(-) diff --git a/src/js/common/dom.js b/src/js/common/dom.js index 76a7e4de..84d4e213 100644 --- a/src/js/common/dom.js +++ b/src/js/common/dom.js @@ -439,7 +439,6 @@ class DOM { } if (isPreview) { - input.attrs.name = `prev-${input.attrs.name}` optionLabel.attrs.contenteditable = true } diff --git a/src/js/components/component.js b/src/js/components/component.js index e8ec1e6c..be9fa5cb 100644 --- a/src/js/components/component.js +++ b/src/js/components/component.js @@ -433,7 +433,6 @@ export default class Component extends Data { from.classList.remove('column-editing-field') } - // make this configurable if (this.name !== 'stage' && !this.children.length) { return this.remove() @@ -584,6 +583,7 @@ export default class Component extends Data { if (this.name === 'column') { parent.autoColumnWidths() } + return newClone } cloneChildren = toParent => { diff --git a/src/js/constants.js b/src/js/constants.js index 5d2eb0a0..1f2047e3 100644 --- a/src/js/constants.js +++ b/src/js/constants.js @@ -175,3 +175,5 @@ export const CONDITION_TEMPLATE = () => ({ }, ], }) + +export const UUID_REGEXP = /(\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)/gi diff --git a/src/js/renderer.js b/src/js/renderer.js index 0ec5c569..bedb3ee3 100644 --- a/src/js/renderer.js +++ b/src/js/renderer.js @@ -1,7 +1,9 @@ import isEqual from 'lodash/isEqual' import dom from './common/dom' import { uuid, isAddress, isExternalAddress } from './common/utils' -import { STAGE_CLASSNAME } from './constants' +import { STAGE_CLASSNAME, UUID_REGEXP } from './constants' + +const RENDER_PREFIX = 'f-' const processOptions = ({ container, ...opts }) => { const processedOptions = { @@ -11,28 +13,13 @@ const processOptions = ({ container, ...opts }) => { return Object.assign({}, opts, processedOptions) } -const baseId = id => id.replace(/^f-/, '') - -const recursiveNewIds = (elem, level = 0) => { - if (!level) { - elem.setAttribute('id', `f-${uuid()}`) - } - const elems = elem.querySelectorAll('*') - const elemsLength = elems.length - for (let i = 0; i < elemsLength; i++) { - const element = elems[i] - if (element.id) { - const label = element.parentElement.querySelector(`[for=${element.id}]`) - const newElementId = `f-${uuid()}` - element.setAttribute('id', newElementId) - if (label) { - label.setAttribute('for', newElementId) - } - } - recursiveNewIds(element, level + 1) - } +const baseId = id => { + const match = id.match(UUID_REGEXP) + return (match && match[0]) || id } +const newUUID = id => id.replace(UUID_REGEXP, uuid()) + const createRemoveButton = () => dom.render( dom.btnTemplate({ @@ -46,30 +33,6 @@ const createRemoveButton = () => }) ) -const addButton = () => - dom.render({ - tag: 'button', - attrs: { - className: 'add-input-group btn pull-right', - type: 'button', - }, - children: 'Add +', - action: { - click: e => { - const fInputGroup = e.target.parentElement - const elem = e.target.previousSibling.cloneNode(true) - recursiveNewIds(elem) - const existingRemoveButton = elem.querySelector('.remove-input-group') - if (existingRemoveButton) { - dom.remove(existingRemoveButton) - } - - fInputGroup.insertBefore(elem, fInputGroup.lastChild) - elem.appendChild(createRemoveButton()) - }, - }, - }) - export default class FormeoRenderer { constructor(opts, formData) { const { renderContainer, external } = processOptions(opts) @@ -96,80 +59,107 @@ export default class FormeoRenderer { this.renderedForm = dom.render(config) dom.empty(this.container) + this.applyConditions() + this.container.appendChild(this.renderedForm) } - orderChildren = (type, order) => - order.reduce((acc, cur) => { - acc.push(this.form[type][cur]) - return acc - }, []) + orderChildren = (type, order) => order.reduce((acc, cur) => [...acc, this.form[type][cur]], []) + + prefixId = id => RENDER_PREFIX + id /** * Convert sizes, apply styles for render * @param {Object} columnData * @return {Object} processed column data */ - processColumnConfig = columnData => { - if (!columnData) { - return - } - const colWidth = columnData.config.width || '100%' - columnData.style = `width: ${colWidth}` - columnData.children = this.processFields(columnData.children) - return dom.render(columnData) - } + processColumn = ({ id, ...columnData }) => + Object.assign({}, columnData, { + id: this.prefixId(id), + children: this.processFields(columnData.children), + style: `width: ${columnData.config.width || '100%'}`, + }) processRows = stageId => - this.orderChildren('rows', this.form.stages[stageId].children).map(row => { - if (!row) { - return - } + this.orderChildren('rows', this.form.stages[stageId].children).reduce( + (acc, row) => (row ? [...acc, this.processRow(row)] : acc), + [] + ) + + cacheComponent = data => { + this.components[baseId(data.id)] = data + return data + } + + /** + * Applies a row's config + * @param {Object} row data + * @return {Object} row config object + */ + processRow = (data, type = 'row') => { + const { config, id } = data + const className = [`formeo-${type}-wrap`] + const rowData = Object.assign({}, data, { children: this.processColumns(data.id), id: this.prefixId(id) }) + this.cacheComponent(rowData) + + const configConditions = [ + { condition: config.legend, result: () => ({ tag: config.fieldset ? 'legend' : 'h3', children: config.legend }) }, + { condition: true, result: () => rowData }, + { condition: config.inputGroup, result: () => this.addButton(id) }, + ] + + const children = configConditions.reduce((acc, { condition, result }) => (condition ? [...acc, result()] : acc), []) + + if (config.inputGroup) { + className.push(RENDER_PREFIX + 'input-group-wrap') + } - row.children = this.processColumns(row.id) + return { + tag: config.fieldset ? 'fieldset' : 'div', + id: uuid(), + className, + children, + } + } - if (row.config.inputGroup) { - return this.makeInputGroup(row) - } + cloneComponentData = componentId => { + const { children = [], id, ...rest } = this.components[componentId] + return Object.assign({}, rest, { + id: newUUID(id), + children: children.length && children.map(({ id }) => this.cloneComponentData(baseId(id))), + }) + } - return row + addButton = id => + dom.render({ + tag: 'button', + attrs: { + className: 'add-input-group btn pull-right', + type: 'button', + }, + children: 'Add +', + action: { + click: e => { + const fInputGroup = e.target.parentElement + const elem = dom.render(this.cloneComponentData(id)) + fInputGroup.insertBefore(elem, fInputGroup.lastChild) + elem.appendChild(createRemoveButton()) + }, + }, }) processColumns = rowId => { - return this.orderChildren('columns', this.form.rows[rowId].children).map(columnConfig => { - if (columnConfig) { - const column = this.processColumnConfig(columnConfig) - this.components[baseId(columnConfig.id)] = column - - return column - } - }) + return this.orderChildren('columns', this.form.rows[rowId].children).map(column => + this.cacheComponent(this.processColumn(column)) + ) } processFields = fieldIds => { - return this.orderChildren('fields', fieldIds).map(child => { - if (child) { - const { conditions } = child - const field = dom.render(child) - this.components[baseId(child.id)] = field - this.processConditions(conditions) - return field - } - }) + return this.orderChildren('fields', fieldIds).map(({ id, ...field }) => + this.cacheComponent(Object.assign({}, field, { id: this.prefixId(id) })) + ) } - /** - * Converts a row to an cloneable input group - * @todo make all columns and fields input groups - * @param {Object} componentData - * @return {NodeElement} inputGroup-ified component - */ - makeInputGroup = data => ({ - id: uuid(), - className: 'f-input-group-wrap', - children: [data, addButton()], - }) - get processedData() { return Object.values(this.form.stages).map(stage => { stage.children = this.processRows(stage.id) @@ -180,33 +170,32 @@ export default class FormeoRenderer { /** * Evaulate and execute conditions for fields by creating listeners for input and changes - * @param {Array} conditions array of arrays of condition definitions * @return {Array} flattened array of conditions */ - processConditions = conditions => { - if (!conditions) { - return null - } - - conditions.forEach((condition, i) => { - const { if: ifConditions, then: thenConditions } = condition - - ifConditions.forEach(ifCondition => { - const { source, ...ifRest } = ifCondition - if (isAddress(source)) { - const component = this.getComponent(source) - const listenerEvent = LISTEN_TYPE_MAP(component) - if (listenerEvent) { - component.addEventListener( - listenerEvent, - evt => - this.evaluateCondition(ifRest, evt) && - thenConditions.forEach(thenCondition => this.execResult(thenCondition, evt)), - false - ) - } - } - }) + applyConditions = () => { + Object.values(this.components).forEach(({ conditions }) => { + if (conditions) { + conditions.forEach((condition, i) => { + const { if: ifConditions, then: thenConditions } = condition + + ifConditions.forEach(ifCondition => { + const { source, ...ifRest } = ifCondition + if (isAddress(source)) { + const component = this.getComponent(source) + const listenerEvent = LISTEN_TYPE_MAP(component) + if (listenerEvent) { + component.addEventListener( + listenerEvent, + evt => + this.evaluateCondition(ifRest, evt) && + thenConditions.forEach(thenCondition => this.execResult(thenCondition, evt)), + false + ) + } + } + }) + }) + } }) } @@ -248,7 +237,7 @@ export default class FormeoRenderer { const componentId = address.slice(address.indexOf('.') + 1) const component = isExternalAddress(address) ? this.external[componentId] - : this.components[componentId].querySelector(`#f-${componentId}`) + : this.renderedForm.querySelector(`#f-${componentId}`) return component } } diff --git a/src/sass/_render.scss b/src/sass/_render.scss index 8f19a2b8..ea76f88b 100644 --- a/src/sass/_render.scss +++ b/src/sass/_render.scss @@ -4,6 +4,20 @@ position: relative; } +.f-input-group-wrap { + > fieldset { + position: relative; + .remove-input-group { + top: 8px; + } + } +} + +.will-remove { + background-color: $remove-bg; + box-shadow: 0 0 1px 0 $brand-error inset; +} + .formeo-row { margin-bottom: 1em; flex-direction: row; @@ -12,18 +26,23 @@ align-content: stretch; align-items: stretch; display: flex; - background-color: transparent; border-radius: $border-radius; - transition: background-color 200ms; - padding: space(); + transition: background-color 200ms, padding 200ms; + padding: space() 0; - &:last-child { + &.will-remove { + padding: space(); + } + + &:last-of-type { margin-bottom: 0; } +} - &.will-remove { - background-color: $remove-bg; - box-shadow: 0 0 3px 1px $brand-error inset; +.formeo-row-wrap { + margin-bottom: 1em; + &:last-child { + margin-bottom: 0; } } @@ -53,15 +72,15 @@ .remove-input-group { position: absolute; - right: -32px; - width: space(3); - height: space(3); - bottom: 0; + right: 0; + top: 0; + width: space(2); + height: space(2); border: 0 none; background: transparent; outline: 0 none; line-height: 0; - padding: space() space(1); + padding: space(); &:hover { .svg-icon { diff --git a/src/sass/base/_variables.scss b/src/sass/base/_variables.scss index 36cb174f..fdb3f276 100644 --- a/src/sass/base/_variables.scss +++ b/src/sass/base/_variables.scss @@ -1,22 +1,22 @@ // Colors -$brand-primary: #325d88 !default; +$brand-primary: rgb(50, 93, 136) !default; $brand-primary-dark: darken($brand-primary, 10%) !default; -$brand-success: #93c54b !default; +$brand-success: rgb(147, 197, 75) !default; $brand-success-dark: darken($brand-success, 10%) !default; -$brand-warning: #f47c3c !default; +$brand-warning: rgb(244, 124, 60) !default; $brand-warning-dark: darken($brand-warning, 10%) !default; -$brand-error: #d9534f !default; +$brand-error: rgb(217, 83, 79) !default; $brand-error-dark: darken($brand-error, 10%) !default; -$brand-info: #9954bb !default; +$brand-info: rgb(153, 84, 187) !default; -$remove-bg: rgba(217, 83, 79, 0.5) !default; +$remove-bg: rgba($brand-error, 0.25) !default; $component-outline-color: #00ffff; $row-outline-color: $component-outline-color; $column-outline-color: darken($component-outline-color, 15%); $field-outline-color: darken($component-outline-color, 30%); -$input-focus: #66afe9; +$input-focus: rgba(102, 175, 233, 1); // Grayscale and brand colors for use across Bootstrap. $black: #000;