UNPKG

@ajsf/core

Version:

Angular JSON Schema Form builder core

1,001 lines 157 kB
import uniqueId from 'lodash/uniqueId'; import cloneDeep from 'lodash/cloneDeep'; import { checkInlineType, getFromSchema, getInputType, isInputRequired, removeRecursiveReferences, updateInputOptions } from './json-schema.functions'; import { copy, fixTitle, forEach, hasOwn } from './utility.functions'; import { inArray, isArray, isDefined, isEmpty, isNumber, isObject, isString } from './validator.functions'; import { JsonPointer } from './jsonpointer.functions'; /** * Layout function library: * * buildLayout: Builds a complete layout from an input layout and schema * * buildLayoutFromSchema: Builds a complete layout entirely from an input schema * * mapLayout: * * getLayoutNode: * * buildTitleMap: */ /** * 'buildLayout' function * * // jsf * // widgetLibrary * // */ export function buildLayout(jsf, widgetLibrary) { let hasSubmitButton = !JsonPointer.get(jsf, '/formOptions/addSubmit'); const formLayout = mapLayout(jsf.layout, (layoutItem, index, layoutPointer) => { const newNode = { _id: uniqueId(), options: {}, }; if (isObject(layoutItem)) { Object.assign(newNode, layoutItem); Object.keys(newNode) .filter(option => !inArray(option, [ '_id', '$ref', 'arrayItem', 'arrayItemType', 'dataPointer', 'dataType', 'items', 'key', 'name', 'options', 'recursiveReference', 'type', 'widget' ])) .forEach(option => { newNode.options[option] = newNode[option]; delete newNode[option]; }); if (!hasOwn(newNode, 'type') && isString(newNode.widget)) { newNode.type = newNode.widget; delete newNode.widget; } if (!hasOwn(newNode.options, 'title')) { if (hasOwn(newNode.options, 'legend')) { newNode.options.title = newNode.options.legend; delete newNode.options.legend; } } if (!hasOwn(newNode.options, 'validationMessages')) { if (hasOwn(newNode.options, 'errorMessages')) { newNode.options.validationMessages = newNode.options.errorMessages; delete newNode.options.errorMessages; // Convert Angular Schema Form (AngularJS) 'validationMessage' to // Angular JSON Schema Form 'validationMessages' // TV4 codes from https://github.com/geraintluff/tv4/blob/master/source/api.js } else if (hasOwn(newNode.options, 'validationMessage')) { if (typeof newNode.options.validationMessage === 'string') { newNode.options.validationMessages = newNode.options.validationMessage; } else { newNode.options.validationMessages = {}; Object.keys(newNode.options.validationMessage).forEach(key => { const code = key + ''; const newKey = code === '0' ? 'type' : code === '1' ? 'enum' : code === '100' ? 'multipleOf' : code === '101' ? 'minimum' : code === '102' ? 'exclusiveMinimum' : code === '103' ? 'maximum' : code === '104' ? 'exclusiveMaximum' : code === '200' ? 'minLength' : code === '201' ? 'maxLength' : code === '202' ? 'pattern' : code === '300' ? 'minProperties' : code === '301' ? 'maxProperties' : code === '302' ? 'required' : code === '304' ? 'dependencies' : code === '400' ? 'minItems' : code === '401' ? 'maxItems' : code === '402' ? 'uniqueItems' : code === '500' ? 'format' : code + ''; newNode.options.validationMessages[newKey] = newNode.options.validationMessage[key]; }); } delete newNode.options.validationMessage; } } } else if (JsonPointer.isJsonPointer(layoutItem)) { newNode.dataPointer = layoutItem; } else if (isString(layoutItem)) { newNode.key = layoutItem; } else { console.error('buildLayout error: Form layout element not recognized:'); console.error(layoutItem); return null; } let nodeSchema = null; // If newNode does not have a dataPointer, try to find an equivalent if (!hasOwn(newNode, 'dataPointer')) { // If newNode has a key, change it to a dataPointer if (hasOwn(newNode, 'key')) { newNode.dataPointer = newNode.key === '*' ? newNode.key : JsonPointer.compile(JsonPointer.parseObjectPath(newNode.key), '-'); delete newNode.key; // If newNode is an array, search for dataPointer in child nodes } else if (hasOwn(newNode, 'type') && newNode.type.slice(-5) === 'array') { const findDataPointer = (items) => { if (items === null || typeof items !== 'object') { return; } if (hasOwn(items, 'dataPointer')) { return items.dataPointer; } if (isArray(items.items)) { for (const item of items.items) { if (hasOwn(item, 'dataPointer') && item.dataPointer.indexOf('/-') !== -1) { return item.dataPointer; } if (hasOwn(item, 'items')) { const searchItem = findDataPointer(item); if (searchItem) { return searchItem; } } } } }; const childDataPointer = findDataPointer(newNode); if (childDataPointer) { newNode.dataPointer = childDataPointer.slice(0, childDataPointer.lastIndexOf('/-')); } } } if (hasOwn(newNode, 'dataPointer')) { if (newNode.dataPointer === '*') { return buildLayoutFromSchema(jsf, widgetLibrary, jsf.formValues); } const nodeValue = JsonPointer.get(jsf.formValues, newNode.dataPointer.replace(/\/-/g, '/1')); // TODO: Create function getFormValues(jsf, dataPointer, forRefLibrary) // check formOptions.setSchemaDefaults and formOptions.setLayoutDefaults // then set apropriate values from initialVaues, schema, or layout newNode.dataPointer = JsonPointer.toGenericPointer(newNode.dataPointer, jsf.arrayMap); const LastKey = JsonPointer.toKey(newNode.dataPointer); if (!newNode.name && isString(LastKey) && LastKey !== '-') { newNode.name = LastKey; } const shortDataPointer = removeRecursiveReferences(newNode.dataPointer, jsf.dataRecursiveRefMap, jsf.arrayMap); const recursive = !shortDataPointer.length || shortDataPointer !== newNode.dataPointer; let schemaPointer; if (!jsf.dataMap.has(shortDataPointer)) { jsf.dataMap.set(shortDataPointer, new Map()); } const nodeDataMap = jsf.dataMap.get(shortDataPointer); if (nodeDataMap.has('schemaPointer')) { schemaPointer = nodeDataMap.get('schemaPointer'); } else { schemaPointer = JsonPointer.toSchemaPointer(shortDataPointer, jsf.schema); nodeDataMap.set('schemaPointer', schemaPointer); } nodeDataMap.set('disabled', !!newNode.options.disabled); nodeSchema = JsonPointer.get(jsf.schema, schemaPointer); if (nodeSchema) { if (!hasOwn(newNode, 'type')) { newNode.type = getInputType(nodeSchema, newNode); } else if (!widgetLibrary.hasWidget(newNode.type)) { const oldWidgetType = newNode.type; newNode.type = getInputType(nodeSchema, newNode); console.error(`error: widget type "${oldWidgetType}" ` + `not found in library. Replacing with "${newNode.type}".`); } else { newNode.type = checkInlineType(newNode.type, nodeSchema, newNode); } if (nodeSchema.type === 'object' && isArray(nodeSchema.required)) { nodeDataMap.set('required', nodeSchema.required); } newNode.dataType = nodeSchema.type || (hasOwn(nodeSchema, '$ref') ? '$ref' : null); updateInputOptions(newNode, nodeSchema, jsf); // Present checkboxes as single control, rather than array if (newNode.type === 'checkboxes' && hasOwn(nodeSchema, 'items')) { updateInputOptions(newNode, nodeSchema.items, jsf); } else if (newNode.dataType === 'array') { newNode.options.maxItems = Math.min(nodeSchema.maxItems || 1000, newNode.options.maxItems || 1000); newNode.options.minItems = Math.max(nodeSchema.minItems || 0, newNode.options.minItems || 0); newNode.options.listItems = Math.max(newNode.options.listItems || 0, isArray(nodeValue) ? nodeValue.length : 0); newNode.options.tupleItems = isArray(nodeSchema.items) ? nodeSchema.items.length : 0; if (newNode.options.maxItems < newNode.options.tupleItems) { newNode.options.tupleItems = newNode.options.maxItems; newNode.options.listItems = 0; } else if (newNode.options.maxItems < newNode.options.tupleItems + newNode.options.listItems) { newNode.options.listItems = newNode.options.maxItems - newNode.options.tupleItems; } else if (newNode.options.minItems > newNode.options.tupleItems + newNode.options.listItems) { newNode.options.listItems = newNode.options.minItems - newNode.options.tupleItems; } if (!nodeDataMap.has('maxItems')) { nodeDataMap.set('maxItems', newNode.options.maxItems); nodeDataMap.set('minItems', newNode.options.minItems); nodeDataMap.set('tupleItems', newNode.options.tupleItems); nodeDataMap.set('listItems', newNode.options.listItems); } if (!jsf.arrayMap.has(shortDataPointer)) { jsf.arrayMap.set(shortDataPointer, newNode.options.tupleItems); } } if (isInputRequired(jsf.schema, schemaPointer)) { newNode.options.required = true; jsf.fieldsRequired = true; } } else { // TODO: create item in FormGroup model from layout key (?) updateInputOptions(newNode, {}, jsf); } if (!newNode.options.title && !/^\d+$/.test(newNode.name)) { newNode.options.title = fixTitle(newNode.name); } if (hasOwn(newNode.options, 'copyValueTo')) { if (typeof newNode.options.copyValueTo === 'string') { newNode.options.copyValueTo = [newNode.options.copyValueTo]; } if (isArray(newNode.options.copyValueTo)) { newNode.options.copyValueTo = newNode.options.copyValueTo.map(item => JsonPointer.compile(JsonPointer.parseObjectPath(item), '-')); } } newNode.widget = widgetLibrary.getWidget(newNode.type); nodeDataMap.set('inputType', newNode.type); nodeDataMap.set('widget', newNode.widget); if (newNode.dataType === 'array' && (hasOwn(newNode, 'items') || hasOwn(newNode, 'additionalItems'))) { const itemRefPointer = removeRecursiveReferences(newNode.dataPointer + '/-', jsf.dataRecursiveRefMap, jsf.arrayMap); if (!jsf.dataMap.has(itemRefPointer)) { jsf.dataMap.set(itemRefPointer, new Map()); } jsf.dataMap.get(itemRefPointer).set('inputType', 'section'); // Fix insufficiently nested array item groups if (newNode.items.length > 1) { const arrayItemGroup = []; for (let i = newNode.items.length - 1; i >= 0; i--) { const subItem = newNode.items[i]; if (hasOwn(subItem, 'dataPointer') && subItem.dataPointer.slice(0, itemRefPointer.length) === itemRefPointer) { const arrayItem = newNode.items.splice(i, 1)[0]; arrayItem.dataPointer = newNode.dataPointer + '/-' + arrayItem.dataPointer.slice(itemRefPointer.length); arrayItemGroup.unshift(arrayItem); } else { subItem.arrayItem = true; // TODO: Check schema to get arrayItemType and removable subItem.arrayItemType = 'list'; subItem.removable = newNode.options.removable !== false; } } if (arrayItemGroup.length) { newNode.items.push({ _id: uniqueId(), arrayItem: true, arrayItemType: newNode.options.tupleItems > newNode.items.length ? 'tuple' : 'list', items: arrayItemGroup, options: { removable: newNode.options.removable !== false, }, dataPointer: newNode.dataPointer + '/-', type: 'section', widget: widgetLibrary.getWidget('section'), }); } } else { // TODO: Fix to hndle multiple items newNode.items[0].arrayItem = true; if (!newNode.items[0].dataPointer) { newNode.items[0].dataPointer = JsonPointer.toGenericPointer(itemRefPointer, jsf.arrayMap); } if (!JsonPointer.has(newNode, '/items/0/options/removable')) { newNode.items[0].options.removable = true; } if (newNode.options.orderable === false) { newNode.items[0].options.orderable = false; } newNode.items[0].arrayItemType = newNode.options.tupleItems ? 'tuple' : 'list'; } if (isArray(newNode.items)) { const arrayListItems = newNode.items.filter(item => item.type !== '$ref').length - newNode.options.tupleItems; if (arrayListItems > newNode.options.listItems) { newNode.options.listItems = arrayListItems; nodeDataMap.set('listItems', arrayListItems); } } if (!hasOwn(jsf.layoutRefLibrary, itemRefPointer)) { jsf.layoutRefLibrary[itemRefPointer] = cloneDeep(newNode.items[newNode.items.length - 1]); if (recursive) { jsf.layoutRefLibrary[itemRefPointer].recursiveReference = true; } forEach(jsf.layoutRefLibrary[itemRefPointer], (item, key) => { if (hasOwn(item, '_id')) { item._id = null; } if (recursive) { if (hasOwn(item, 'dataPointer')) { item.dataPointer = item.dataPointer.slice(itemRefPointer.length); } } }, 'top-down'); } // Add any additional default items if (!newNode.recursiveReference || newNode.options.required) { const arrayLength = Math.min(Math.max(newNode.options.tupleItems + newNode.options.listItems, isArray(nodeValue) ? nodeValue.length : 0), newNode.options.maxItems); for (let i = newNode.items.length; i < arrayLength; i++) { newNode.items.push(getLayoutNode({ $ref: itemRefPointer, dataPointer: newNode.dataPointer, recursiveReference: newNode.recursiveReference, }, jsf, widgetLibrary)); } } // If needed, add button to add items to array if (newNode.options.addable !== false && newNode.options.minItems < newNode.options.maxItems && (newNode.items[newNode.items.length - 1] || {}).type !== '$ref') { let buttonText = 'Add'; if (newNode.options.title) { if (/^add\b/i.test(newNode.options.title)) { buttonText = newNode.options.title; } else { buttonText += ' ' + newNode.options.title; } } else if (newNode.name && !/^\d+$/.test(newNode.name)) { if (/^add\b/i.test(newNode.name)) { buttonText += ' ' + fixTitle(newNode.name); } else { buttonText = fixTitle(newNode.name); } // If newNode doesn't have a title, look for title of parent array item } else { const parentSchema = getFromSchema(jsf.schema, newNode.dataPointer, 'parentSchema'); if (hasOwn(parentSchema, 'title')) { buttonText += ' to ' + parentSchema.title; } else { const pointerArray = JsonPointer.parse(newNode.dataPointer); buttonText += ' to ' + fixTitle(pointerArray[pointerArray.length - 2]); } } newNode.items.push({ _id: uniqueId(), arrayItem: true, arrayItemType: 'list', dataPointer: newNode.dataPointer + '/-', options: { listItems: newNode.options.listItems, maxItems: newNode.options.maxItems, minItems: newNode.options.minItems, removable: false, title: buttonText, tupleItems: newNode.options.tupleItems, }, recursiveReference: recursive, type: '$ref', widget: widgetLibrary.getWidget('$ref'), $ref: itemRefPointer, }); if (isString(JsonPointer.get(newNode, '/style/add'))) { newNode.items[newNode.items.length - 1].options.fieldStyle = newNode.style.add; delete newNode.style.add; if (isEmpty(newNode.style)) { delete newNode.style; } } } } else { newNode.arrayItem = false; } } else if (hasOwn(newNode, 'type') || hasOwn(newNode, 'items')) { const parentType = JsonPointer.get(jsf.layout, layoutPointer, 0, -2).type; if (!hasOwn(newNode, 'type')) { newNode.type = inArray(parentType, ['tabs', 'tabarray']) ? 'tab' : 'array'; } newNode.arrayItem = parentType === 'array'; newNode.widget = widgetLibrary.getWidget(newNode.type); updateInputOptions(newNode, {}, jsf); } if (newNode.type === 'submit') { hasSubmitButton = true; } return newNode; }); if (jsf.hasRootReference) { const fullLayout = cloneDeep(formLayout); if (fullLayout[fullLayout.length - 1].type === 'submit') { fullLayout.pop(); } jsf.layoutRefLibrary[''] = { _id: null, dataPointer: '', dataType: 'object', items: fullLayout, name: '', options: cloneDeep(jsf.formOptions.defautWidgetOptions), recursiveReference: true, required: false, type: 'section', widget: widgetLibrary.getWidget('section'), }; } if (!hasSubmitButton) { formLayout.push({ _id: uniqueId(), options: { title: 'Submit' }, type: 'submit', widget: widgetLibrary.getWidget('submit'), }); } return formLayout; } /** * 'buildLayoutFromSchema' function * * // jsf - * // widgetLibrary - * // nodeValue - * // { string = '' } schemaPointer - * // { string = '' } dataPointer - * // { boolean = false } arrayItem - * // { string = null } arrayItemType - * // { boolean = null } removable - * // { boolean = false } forRefLibrary - * // { string = '' } dataPointerPrefix - * // */ export function buildLayoutFromSchema(jsf, widgetLibrary, nodeValue = null, schemaPointer = '', dataPointer = '', arrayItem = false, arrayItemType = null, removable = null, forRefLibrary = false, dataPointerPrefix = '') { const schema = JsonPointer.get(jsf.schema, schemaPointer); if (!hasOwn(schema, 'type') && !hasOwn(schema, '$ref') && !hasOwn(schema, 'x-schema-form')) { return null; } const newNodeType = getInputType(schema); if (!isDefined(nodeValue) && (jsf.formOptions.setSchemaDefaults === true || (jsf.formOptions.setSchemaDefaults === 'auto' && isEmpty(jsf.formValues)))) { nodeValue = JsonPointer.get(jsf.schema, schemaPointer + '/default'); } let newNode = { _id: forRefLibrary ? null : uniqueId(), arrayItem: arrayItem, dataPointer: JsonPointer.toGenericPointer(dataPointer, jsf.arrayMap), dataType: schema.type || (hasOwn(schema, '$ref') ? '$ref' : null), options: {}, required: isInputRequired(jsf.schema, schemaPointer), type: newNodeType, widget: widgetLibrary.getWidget(newNodeType), }; const lastDataKey = JsonPointer.toKey(newNode.dataPointer); if (lastDataKey !== '-') { newNode.name = lastDataKey; } if (newNode.arrayItem) { newNode.arrayItemType = arrayItemType; newNode.options.removable = removable !== false; } const shortDataPointer = removeRecursiveReferences(dataPointerPrefix + dataPointer, jsf.dataRecursiveRefMap, jsf.arrayMap); const recursive = !shortDataPointer.length || shortDataPointer !== dataPointerPrefix + dataPointer; if (!jsf.dataMap.has(shortDataPointer)) { jsf.dataMap.set(shortDataPointer, new Map()); } const nodeDataMap = jsf.dataMap.get(shortDataPointer); if (!nodeDataMap.has('inputType')) { nodeDataMap.set('schemaPointer', schemaPointer); nodeDataMap.set('inputType', newNode.type); nodeDataMap.set('widget', newNode.widget); nodeDataMap.set('disabled', !!newNode.options.disabled); } updateInputOptions(newNode, schema, jsf); if (!newNode.options.title && newNode.name && !/^\d+$/.test(newNode.name)) { newNode.options.title = fixTitle(newNode.name); } if (newNode.dataType === 'object') { if (isArray(schema.required) && !nodeDataMap.has('required')) { nodeDataMap.set('required', schema.required); } if (isObject(schema.properties)) { const newSection = []; const propertyKeys = schema['ui:order'] || Object.keys(schema.properties); if (propertyKeys.includes('*') && !hasOwn(schema.properties, '*')) { const unnamedKeys = Object.keys(schema.properties) .filter(key => !propertyKeys.includes(key)); for (let i = propertyKeys.length - 1; i >= 0; i--) { if (propertyKeys[i] === '*') { propertyKeys.splice(i, 1, ...unnamedKeys); } } } propertyKeys .filter(key => hasOwn(schema.properties, key) || hasOwn(schema, 'additionalProperties')) .forEach(key => { const keySchemaPointer = hasOwn(schema.properties, key) ? '/properties/' + key : '/additionalProperties'; const innerItem = buildLayoutFromSchema(jsf, widgetLibrary, isObject(nodeValue) ? nodeValue[key] : null, schemaPointer + keySchemaPointer, dataPointer + '/' + key, false, null, null, forRefLibrary, dataPointerPrefix); if (innerItem) { if (isInputRequired(schema, '/' + key)) { innerItem.options.required = true; jsf.fieldsRequired = true; } newSection.push(innerItem); } }); if (dataPointer === '' && !forRefLibrary) { newNode = newSection; } else { newNode.items = newSection; } } // TODO: Add patternProperties and additionalProperties inputs? // ... possibly provide a way to enter both key names and values? // if (isObject(schema.patternProperties)) { } // if (isObject(schema.additionalProperties)) { } } else if (newNode.dataType === 'array') { newNode.items = []; newNode.options.maxItems = Math.min(schema.maxItems || 1000, newNode.options.maxItems || 1000); newNode.options.minItems = Math.max(schema.minItems || 0, newNode.options.minItems || 0); if (!newNode.options.minItems && isInputRequired(jsf.schema, schemaPointer)) { newNode.options.minItems = 1; } if (!hasOwn(newNode.options, 'listItems')) { newNode.options.listItems = 1; } newNode.options.tupleItems = isArray(schema.items) ? schema.items.length : 0; if (newNode.options.maxItems <= newNode.options.tupleItems) { newNode.options.tupleItems = newNode.options.maxItems; newNode.options.listItems = 0; } else if (newNode.options.maxItems < newNode.options.tupleItems + newNode.options.listItems) { newNode.options.listItems = newNode.options.maxItems - newNode.options.tupleItems; } else if (newNode.options.minItems > newNode.options.tupleItems + newNode.options.listItems) { newNode.options.listItems = newNode.options.minItems - newNode.options.tupleItems; } if (!nodeDataMap.has('maxItems')) { nodeDataMap.set('maxItems', newNode.options.maxItems); nodeDataMap.set('minItems', newNode.options.minItems); nodeDataMap.set('tupleItems', newNode.options.tupleItems); nodeDataMap.set('listItems', newNode.options.listItems); } if (!jsf.arrayMap.has(shortDataPointer)) { jsf.arrayMap.set(shortDataPointer, newNode.options.tupleItems); } removable = newNode.options.removable !== false; let additionalItemsSchemaPointer = null; // If 'items' is an array = tuple items if (isArray(schema.items)) { newNode.items = []; for (let i = 0; i < newNode.options.tupleItems; i++) { let newItem; const itemRefPointer = removeRecursiveReferences(shortDataPointer + '/' + i, jsf.dataRecursiveRefMap, jsf.arrayMap); const itemRecursive = !itemRefPointer.length || itemRefPointer !== shortDataPointer + '/' + i; // If removable, add tuple item layout to layoutRefLibrary if (removable && i >= newNode.options.minItems) { if (!hasOwn(jsf.layoutRefLibrary, itemRefPointer)) { // Set to null first to prevent recursive reference from causing endless loop jsf.layoutRefLibrary[itemRefPointer] = null; jsf.layoutRefLibrary[itemRefPointer] = buildLayoutFromSchema(jsf, widgetLibrary, isArray(nodeValue) ? nodeValue[i] : null, schemaPointer + '/items/' + i, itemRecursive ? '' : dataPointer + '/' + i, true, 'tuple', true, true, itemRecursive ? dataPointer + '/' + i : ''); if (itemRecursive) { jsf.layoutRefLibrary[itemRefPointer].recursiveReference = true; } } newItem = getLayoutNode({ $ref: itemRefPointer, dataPointer: dataPointer + '/' + i, recursiveReference: itemRecursive, }, jsf, widgetLibrary, isArray(nodeValue) ? nodeValue[i] : null); } else { newItem = buildLayoutFromSchema(jsf, widgetLibrary, isArray(nodeValue) ? nodeValue[i] : null, schemaPointer + '/items/' + i, dataPointer + '/' + i, true, 'tuple', false, forRefLibrary, dataPointerPrefix); } if (newItem) { newNode.items.push(newItem); } } // If 'additionalItems' is an object = additional list items, after tuple items if (isObject(schema.additionalItems)) { additionalItemsSchemaPointer = schemaPointer + '/additionalItems'; } // If 'items' is an object = list items only (no tuple items) } else if (isObject(schema.items)) { additionalItemsSchemaPointer = schemaPointer + '/items'; } if (additionalItemsSchemaPointer) { const itemRefPointer = removeRecursiveReferences(shortDataPointer + '/-', jsf.dataRecursiveRefMap, jsf.arrayMap); const itemRecursive = !itemRefPointer.length || itemRefPointer !== shortDataPointer + '/-'; const itemSchemaPointer = removeRecursiveReferences(additionalItemsSchemaPointer, jsf.schemaRecursiveRefMap, jsf.arrayMap); // Add list item layout to layoutRefLibrary if (itemRefPointer.length && !hasOwn(jsf.layoutRefLibrary, itemRefPointer)) { // Set to null first to prevent recursive reference from causing endless loop jsf.layoutRefLibrary[itemRefPointer] = null; jsf.layoutRefLibrary[itemRefPointer] = buildLayoutFromSchema(jsf, widgetLibrary, null, itemSchemaPointer, itemRecursive ? '' : dataPointer + '/-', true, 'list', removable, true, itemRecursive ? dataPointer + '/-' : ''); if (itemRecursive) { jsf.layoutRefLibrary[itemRefPointer].recursiveReference = true; } } // Add any additional default items if (!itemRecursive || newNode.options.required) { const arrayLength = Math.min(Math.max(itemRecursive ? 0 : newNode.options.tupleItems + newNode.options.listItems, isArray(nodeValue) ? nodeValue.length : 0), newNode.options.maxItems); if (newNode.items.length < arrayLength) { for (let i = newNode.items.length; i < arrayLength; i++) { newNode.items.push(getLayoutNode({ $ref: itemRefPointer, dataPointer: dataPointer + '/-', recursiveReference: itemRecursive, }, jsf, widgetLibrary, isArray(nodeValue) ? nodeValue[i] : null)); } } } // If needed, add button to add items to array if (newNode.options.addable !== false && newNode.options.minItems < newNode.options.maxItems && (newNode.items[newNode.items.length - 1] || {}).type !== '$ref') { let buttonText = ((jsf.layoutRefLibrary[itemRefPointer] || {}).options || {}).title; const prefix = buttonText ? 'Add ' : 'Add to '; if (!buttonText) { buttonText = schema.title || fixTitle(JsonPointer.toKey(dataPointer)); } if (!/^add\b/i.test(buttonText)) { buttonText = prefix + buttonText; } newNode.items.push({ _id: uniqueId(), arrayItem: true, arrayItemType: 'list', dataPointer: newNode.dataPointer + '/-', options: { listItems: newNode.options.listItems, maxItems: newNode.options.maxItems, minItems: newNode.options.minItems, removable: false, title: buttonText, tupleItems: newNode.options.tupleItems, }, recursiveReference: itemRecursive, type: '$ref', widget: widgetLibrary.getWidget('$ref'), $ref: itemRefPointer, }); } } } else if (newNode.dataType === '$ref') { const schemaRef = JsonPointer.compile(schema.$ref); const dataRef = JsonPointer.toDataPointer(schemaRef, jsf.schema); let buttonText = ''; // Get newNode title if (newNode.options.add) { buttonText = newNode.options.add; } else if (newNode.name && !/^\d+$/.test(newNode.name)) { buttonText = (/^add\b/i.test(newNode.name) ? '' : 'Add ') + fixTitle(newNode.name); // If newNode doesn't have a title, look for title of parent array item } else { const parentSchema = JsonPointer.get(jsf.schema, schemaPointer, 0, -1); if (hasOwn(parentSchema, 'title')) { buttonText = 'Add to ' + parentSchema.title; } else { const pointerArray = JsonPointer.parse(newNode.dataPointer); buttonText = 'Add to ' + fixTitle(pointerArray[pointerArray.length - 2]); } } Object.assign(newNode, { recursiveReference: true, widget: widgetLibrary.getWidget('$ref'), $ref: dataRef, }); Object.assign(newNode.options, { removable: false, title: buttonText, }); if (isNumber(JsonPointer.get(jsf.schema, schemaPointer, 0, -1).maxItems)) { newNode.options.maxItems = JsonPointer.get(jsf.schema, schemaPointer, 0, -1).maxItems; } // Add layout template to layoutRefLibrary if (dataRef.length) { if (!hasOwn(jsf.layoutRefLibrary, dataRef)) { // Set to null first to prevent recursive reference from causing endless loop jsf.layoutRefLibrary[dataRef] = null; const newLayout = buildLayoutFromSchema(jsf, widgetLibrary, null, schemaRef, '', newNode.arrayItem, newNode.arrayItemType, true, true, dataPointer); if (newLayout) { newLayout.recursiveReference = true; jsf.layoutRefLibrary[dataRef] = newLayout; } else { delete jsf.layoutRefLibrary[dataRef]; } } else if (!jsf.layoutRefLibrary[dataRef].recursiveReference) { jsf.layoutRefLibrary[dataRef].recursiveReference = true; } } } return newNode; } /** * 'mapLayout' function * * Creates a new layout by running each element in an existing layout through * an iteratee. Recursively maps within array elements 'items' and 'tabs'. * The iteratee is invoked with four arguments: (value, index, layout, path) * * The returned layout may be longer (or shorter) then the source layout. * * If an item from the source layout returns multiple items (as '*' usually will), * this function will keep all returned items in-line with the surrounding items. * * If an item from the source layout causes an error and returns null, it is * skipped without error, and the function will still return all non-null items. * * // layout - the layout to map * // { (v: any, i?: number, l?: any, p?: string) => any } * function - the funciton to invoke on each element * // { string|string[] = '' } layoutPointer - the layoutPointer to layout, inside rootLayout * // { any[] = layout } rootLayout - the root layout, which conatins layout * // */ export function mapLayout(layout, fn, layoutPointer = '', rootLayout = layout) { let indexPad = 0; let newLayout = []; forEach(layout, (item, index) => { const realIndex = +index + indexPad; const newLayoutPointer = layoutPointer + '/' + realIndex; let newNode = copy(item); let itemsArray = []; if (isObject(item)) { if (hasOwn(item, 'tabs')) { item.items = item.tabs; delete item.tabs; } if (hasOwn(item, 'items')) { itemsArray = isArray(item.items) ? item.items : [item.items]; } } if (itemsArray.length) { newNode.items = mapLayout(itemsArray, fn, newLayoutPointer + '/items', rootLayout); } newNode = fn(newNode, realIndex, newLayoutPointer, rootLayout); if (!isDefined(newNode)) { indexPad--; } else { if (isArray(newNode)) { indexPad += newNode.length - 1; } newLayout = newLayout.concat(newNode); } }); return newLayout; } /** * 'getLayoutNode' function * Copy a new layoutNode from layoutRefLibrary * * // refNode - * // layoutRefLibrary - * // { any = null } widgetLibrary - * // { any = null } nodeValue - * // copied layoutNode */ export function getLayoutNode(refNode, jsf, widgetLibrary = null, nodeValue = null) { // If recursive reference and building initial layout, return Add button if (refNode.recursiveReference && widgetLibrary) { const newLayoutNode = cloneDeep(refNode); if (!newLayoutNode.options) { newLayoutNode.options = {}; } Object.assign(newLayoutNode, { recursiveReference: true, widget: widgetLibrary.getWidget('$ref'), }); Object.assign(newLayoutNode.options, { removable: false, title: 'Add ' + newLayoutNode.$ref, }); return newLayoutNode; // Otherwise, return referenced layout } else { let newLayoutNode = jsf.layoutRefLibrary[refNode.$ref]; // If value defined, build new node from schema (to set array lengths) if (isDefined(nodeValue)) { newLayoutNode = buildLayoutFromSchema(jsf, widgetLibrary, nodeValue, JsonPointer.toSchemaPointer(refNode.$ref, jsf.schema), refNode.$ref, newLayoutNode.arrayItem, newLayoutNode.arrayItemType, newLayoutNode.options.removable, false); } else { // If value not defined, copy node from layoutRefLibrary newLayoutNode = cloneDeep(newLayoutNode); JsonPointer.forEachDeep(newLayoutNode, (subNode, pointer) => { // Reset all _id's in newLayoutNode to unique values if (hasOwn(subNode, '_id')) { subNode._id = uniqueId(); } // If adding a recursive item, prefix current dataPointer // to all dataPointers in new layoutNode if (refNode.recursiveReference && hasOwn(subNode, 'dataPointer')) { subNode.dataPointer = refNode.dataPointer + subNode.dataPointer; } }); } return newLayoutNode; } } /** * 'buildTitleMap' function * * // titleMap - * // enumList - * // { boolean = true } fieldRequired - * // { boolean = true } flatList - * // { TitleMapItem[] } */ export function buildTitleMap(titleMap, enumList, fieldRequired = true, flatList = true) { let newTitleMap = []; let hasEmptyValue = false; if (titleMap) { if (isArray(titleMap)) { if (enumList) { for (const i of Object.keys(titleMap)) { if (isObject(titleMap[i])) { // JSON Form style const value = titleMap[i].value; if (enumList.includes(value)) { const name = titleMap[i].name; newTitleMap.push({ name, value }); if (value === undefined || value === null) { hasEmptyValue = true; } } } else if (isString(titleMap[i])) { // React Jsonschema Form style if (i < enumList.length) { const name = titleMap[i]; const value = enumList[i]; newTitleMap.push({ name, value }); if (value === undefined || value === null) { hasEmptyValue = true; } } } } } else { // If array titleMap and no enum list, just return the titleMap - Angular Schema Form style newTitleMap = titleMap; if (!fieldRequired) { hasEmptyValue = !!newTitleMap .filter(i => i.value === undefined || i.value === null) .length; } } } else if (enumList) { // Alternate JSON Form style, with enum list for (const i of Object.keys(enumList)) { const value = enumList[i]; if (hasOwn(titleMap, value)) { const name = titleMap[value]; newTitleMap.push({ name, value }); if (value === undefined || value === null) { hasEmptyValue = true; } } } } else { // Alternate JSON Form style, without enum list for (const value of Object.keys(titleMap)) { const name = titleMap[value]; newTitleMap.push({ name, value }); if (value === undefined || value === null) { hasEmptyValue = true; } } } } else if (enumList) { // Build map from enum list alone for (const i of Object.keys(enumList)) { const name = enumList[i]; const value = enumList[i]; newTitleMap.push({ name, value }); if (value === undefined || value === null) { hasEmptyValue = true; } } } else { // If no titleMap and no enum list, return default map of boolean values newTitleMap = [{ name: 'True', value: true }, { name: 'False', value: false }]; } // Does titleMap have groups? if (newTitleMap.some(title => hasOwn(title, 'group'))) { hasEmptyValue = false; // If flatList = true, flatten items & update name to group: name if (flatList) { newTitleMap = newTitleMap.reduce((groupTitleMap, title) => { if (hasOwn(title, 'group')) { if (isArray(title.items)) { groupTitleMap = [ ...groupTitleMap, ...title.items.map(item => ({ ...item, ...{ name: `${title.group}: ${item.name}` } })) ]; if (title.items.some(item => item.value === undefined || item.value === null)) { hasEmptyValue = true; } } if (hasOwn(title, 'name') && hasOwn(title, 'value')) { title.name = `${title.group}: ${title.name}`; delete title.group; groupTitleMap.push(title); if (title.value === undefined || title.value === null) { hasEmptyValue = true; } } } else { groupTitleMap.push(title); if (title.value === undefined || title.value === null) { hasEmptyValue = true; } } return groupTitleMap; }, []); // If flatList = false, combine items from matching groups } else { newTitleMap = newTitleMap.reduce((groupTitleMap, title) => { if (hasOwn(title, 'group')) { if (title.group !== (groupTitleMap[groupTitleMap.length - 1] || {}).group) { groupTitleMap.push({ group: title.group, items: title.items || [] }); } if (hasOwn(title, 'name') && hasOwn(title, 'value')) { groupTitleMap[groupTitleMap.length - 1].items .push({ name: title.name, value: title.value }); if (title.value === undefined || title.value === null) { hasEmptyValue = true; } } } else { groupTitleMap.push(title); if (title.value === undefined || title.value === null) { hasEmptyValue = true; } } return groupTitleMap; }, []); } } if (!fieldRequired && !hasEmptyValue) { newTitleMap.unshift({ name: '<em>None</em>', value: null }); } return newTitleMap; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGF5b3V0LmZ1bmN0aW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2Fqc2YtY29yZS9zcmMvbGliL3NoYXJlZC9sYXlvdXQuZnVuY3Rpb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sUUFBUSxNQUFNLGlCQUFpQixDQUFDO0FBQ3ZDLE9BQU8sU0FBUyxNQUFNLGtCQUFrQixDQUFDO0FBQ3pDLE9BQU8sRUFDTCxlQUFlLEVBQ2YsYUFBYSxFQUNiLFlBQVksRUFDWixlQUFlLEVBQ2YseUJBQXlCLEVBQ3pCLGtCQUFrQixFQUNqQixNQUFNLHlCQUF5QixDQUFDO0FBQ25DLE9BQU8sRUFDTCxJQUFJLEVBQ0osUUFBUSxFQUNSLE9BQU8sRUFDUCxNQUFNLEVBQ0wsTUFBTSxxQkFBcUIsQ0FBQztBQUMvQixPQUFPLEVBQ0wsT0FBTyxFQUNQLE9BQU8sRUFDUCxTQUFTLEVBQ1QsT0FBTyxFQUNQLFFBQVEsRUFDUixRQUFRLEVBQ1IsUUFBUSxFQUNQLE1BQU0sdUJBQXVCLENBQUM7QUFDakMsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBS3REOzs7Ozs7Ozs7Ozs7R0FZRztBQUVIOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxXQUFXLENBQUMsR0FBRyxFQUFFLGFBQWE7SUFDNUMsSUFBSSxlQUFlLEdBQUcsQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSx3QkFBd0IsQ0FBQyxDQUFDO0lBQ3RFLE1BQU0sVUFBVSxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLENBQUMsVUFBVSxFQUFFLEtBQUssRUFBRSxhQUFhLEVBQUUsRUFBRTtRQUM1RSxNQUFNLE9BQU8sR0FBUTtZQUNuQix