formiojs
Version:
Common js library for client side interaction with <form.io>
668 lines (550 loc) • 18.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.applyFormChanges = applyFormChanges;
exports.eachComponent = eachComponent;
exports.escapeRegExCharacters = escapeRegExCharacters;
exports.findComponent = findComponent;
exports.findComponents = findComponents;
exports.flattenComponents = flattenComponents;
exports.formatAsCurrency = formatAsCurrency;
exports.generateFormChange = generateFormChange;
exports.getComponent = getComponent;
exports.getStrings = getStrings;
exports.getValue = getValue;
exports.hasCondition = hasCondition;
exports.isLayoutComponent = isLayoutComponent;
exports.matchComponent = matchComponent;
exports.parseFloatExt = parseFloatExt;
exports.removeComponent = removeComponent;
exports.searchComponents = searchComponents;
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/web.dom-collections.for-each.js");
require("core-js/modules/es.array.concat.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.array.slice.js");
require("core-js/modules/es.array.splice.js");
require("core-js/modules/es.regexp.exec.js");
require("core-js/modules/es.string.replace.js");
require("core-js/modules/es.string.split.js");
require("core-js/modules/es.regexp.to-string.js");
require("core-js/modules/es.array.join.js");
require("core-js/modules/es.array.map.js");
require("core-js/modules/es.array.from.js");
require("core-js/modules/es.string.iterator.js");
require("core-js/modules/es.array.iterator.js");
require("core-js/modules/web.dom-collections.iterator.js");
var _get = _interopRequireDefault(require("lodash/get"));
var _set = _interopRequireDefault(require("lodash/set"));
var _has = _interopRequireDefault(require("lodash/has"));
var _clone = _interopRequireDefault(require("lodash/clone"));
var _forOwn = _interopRequireDefault(require("lodash/forOwn"));
var _isString = _interopRequireDefault(require("lodash/isString"));
var _isNaN = _interopRequireDefault(require("lodash/isNaN"));
var _isNil = _interopRequireDefault(require("lodash/isNil"));
var _isPlainObject = _interopRequireDefault(require("lodash/isPlainObject"));
var _round = _interopRequireDefault(require("lodash/round"));
var _chunk = _interopRequireDefault(require("lodash/chunk"));
var _pad = _interopRequireDefault(require("lodash/pad"));
var _fastJsonPatch = require("fast-json-patch");
var _lodash = _interopRequireDefault(require("lodash"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Determine if a component is a layout component or not.
*
* @param {Object} component
* The component to check.
*
* @returns {Boolean}
* Whether or not the component is a layout component.
*/
function isLayoutComponent(component) {
return Boolean(component.columns && Array.isArray(component.columns) || component.rows && Array.isArray(component.rows) || component.components && Array.isArray(component.components));
}
/**
* Iterate through each component within a form.
*
* @param {Object} components
* The components to iterate.
* @param {Function} fn
* The iteration function to invoke for each component.
* @param {Boolean} includeAll
* Whether or not to include layout components.
* @param {String} path
* The current data path of the element. Example: data.user.firstName
* @param {Object} parent
* The parent object.
*/
function eachComponent(components, fn, includeAll, path, parent, inRecursion) {
if (!components) return;
path = path || '';
if (inRecursion) {
if (components.noRecurse) {
delete components.noRecurse;
return;
}
components.noRecurse = true;
}
components.forEach(function (component) {
if (!component) {
return;
}
var hasColumns = component.columns && Array.isArray(component.columns);
var hasRows = component.rows && Array.isArray(component.rows);
var hasComps = component.components && Array.isArray(component.components);
var noRecurse = false;
var newPath = component.key ? path ? "".concat(path, ".").concat(component.key) : component.key : ''; // Keep track of parent references.
if (parent) {
// Ensure we don't create infinite JSON structures.
component.parent = (0, _clone.default)(parent);
delete component.parent.components;
delete component.parent.componentMap;
delete component.parent.columns;
delete component.parent.rows;
} // there's no need to add other layout components here because we expect that those would either have columns, rows or components
var layoutTypes = ['htmlelement', 'content'];
var isLayoutComponent = hasColumns || hasRows || hasComps && !component.input || layoutTypes.indexOf(component.type) > -1;
if (includeAll || component.tree || !isLayoutComponent) {
noRecurse = fn(component, newPath, components);
}
var subPath = function subPath() {
if (component.key && !['panel', 'table', 'well', 'columns', 'fieldset', 'tabs', 'form'].includes(component.type) && (['datagrid', 'container', 'editgrid', 'address', 'dynamicWizard', 'datatable'].includes(component.type) || component.tree)) {
return newPath;
} else if (component.key && component.type === 'form') {
return "".concat(newPath, ".data");
}
return path;
};
if (!noRecurse) {
if (hasColumns) {
component.columns.forEach(function (column) {
return eachComponent(column.components, fn, includeAll, subPath(), parent ? component : null);
}, true);
} else if (hasRows) {
component.rows.forEach(function (row) {
if (Array.isArray(row)) {
row.forEach(function (column) {
return eachComponent(column.components, fn, includeAll, subPath(), parent ? component : null);
}, true);
}
});
} else if (hasComps) {
eachComponent(component.components, fn, includeAll, subPath(), parent ? component : null, true);
}
}
});
if (components.noRecurse) {
delete components.noRecurse;
}
}
/**
* Matches if a component matches the query.
*
* @param component
* @param query
* @return {boolean}
*/
function matchComponent(component, query) {
if ((0, _isString.default)(query)) {
return component.key === query || component.path === query;
} else {
var matches = false;
(0, _forOwn.default)(query, function (value, key) {
matches = (0, _get.default)(component, key) === value;
if (!matches) {
return false;
}
});
return matches;
}
}
/**
* Get a component by its key
*
* @param {Object} components
* The components to iterate.
* @param {String|Object} key
* The key of the component to get, or a query of the component to search.
*
* @returns {Object}
* The component that matches the given key, or undefined if not found.
*/
function getComponent(components, key, includeAll) {
var result;
eachComponent(components, function (component, path) {
if (path === key || component.path === key) {
result = component;
return true;
}
}, includeAll);
return result;
}
/**
* Finds a component provided a query of properties of that component.
*
* @param components
* @param query
* @return {*}
*/
function searchComponents(components, query) {
var results = [];
eachComponent(components, function (component) {
if (matchComponent(component, query)) {
results.push(component);
}
}, true);
return results;
}
/**
* Deprecated version of findComponents. Renamed to searchComponents.
*
* @param components
* @param query
* @returns {*}
*/
function findComponents(components, query) {
console.warn('formio.js/utils findComponents is deprecated. Use searchComponents instead.');
return searchComponents(components, query);
}
/**
* This function will find a component in a form and return the component AND THE PATH to the component in the form.
* Path to the component is stored as an array of nested components and their indexes.The Path is being filled recursively
* when you iterating through the nested structure.
* If the component is not found the callback won't be called and function won't return anything.
*
* @param components
* @param key
* @param fn
* @param path
* @returns {*}
*/
function findComponent(components, key, path, fn) {
if (!components) return;
path = path || [];
if (!key) {
return fn(components);
}
components.forEach(function (component, index) {
var newPath = path.slice(); // Add an index of the component it iterates through in nested structure
newPath.push(index);
if (!component) return;
if (component.hasOwnProperty('columns') && Array.isArray(component.columns)) {
newPath.push('columns');
component.columns.forEach(function (column, index) {
var colPath = newPath.slice();
colPath.push(index);
colPath.push('components');
findComponent(column.components, key, colPath, fn);
});
}
if (component.hasOwnProperty('rows') && Array.isArray(component.rows)) {
newPath.push('rows');
component.rows.forEach(function (row, index) {
var rowPath = newPath.slice();
rowPath.push(index);
row.forEach(function (column, index) {
var colPath = rowPath.slice();
colPath.push(index);
colPath.push('components');
findComponent(column.components, key, colPath, fn);
});
});
}
if (component.hasOwnProperty('components') && Array.isArray(component.components)) {
newPath.push('components');
findComponent(component.components, key, newPath, fn);
}
if (component.key === key) {
//Final callback if the component is found
fn(component, newPath, components);
}
});
}
/**
* Remove a component by path.
*
* @param components
* @param path
*/
function removeComponent(components, path) {
// Using _.unset() leave a null value. Use Array splice instead.
var index = path.pop();
if (path.length !== 0) {
components = (0, _get.default)(components, path);
}
components.splice(index, 1);
}
function generateFormChange(type, data) {
var change;
switch (type) {
case 'add':
change = {
op: 'add',
key: data.component.key,
container: data.parent.key,
// Parent component
path: data.path,
// Path to container within parent component.
index: data.index,
// Index of component in parent container.
component: data.component
};
break;
case 'edit':
change = {
op: 'edit',
key: data.originalComponent.key,
patches: (0, _fastJsonPatch.compare)(data.originalComponent, data.component)
}; // Don't save if nothing changed.
if (!change.patches.length) {
change = null;
}
break;
case 'remove':
change = {
op: 'remove',
key: data.component.key
};
break;
}
return change;
}
function applyFormChanges(form, changes) {
var failed = [];
changes.forEach(function (change) {
var found = false;
switch (change.op) {
case 'add':
var newComponent = change.component; // Find the container to set the component in.
findComponent(form.components, change.container, null, function (parent) {
if (!change.container) {
parent = form;
} // A move will first run an add so remove any existing components with matching key before inserting.
findComponent(form.components, change.key, null, function (component, path) {
// If found, use the existing component. (If someone else edited it, the changes would be here)
newComponent = component;
removeComponent(form.components, path);
});
found = true;
var container = (0, _get.default)(parent, change.path);
container.splice(change.index, 0, newComponent);
});
break;
case 'remove':
findComponent(form.components, change.key, null, function (component, path) {
found = true;
var oldComponent = (0, _get.default)(form.components, path);
if (oldComponent.key !== component.key) {
path.pop();
}
removeComponent(form.components, path);
});
break;
case 'edit':
findComponent(form.components, change.key, null, function (component, path) {
found = true;
try {
var oldComponent = (0, _get.default)(form.components, path);
var _newComponent = (0, _fastJsonPatch.applyPatch)(component, change.patches).newDocument;
if (oldComponent.key !== _newComponent.key) {
path.pop();
}
(0, _set.default)(form.components, path, _newComponent);
} catch (err) {
failed.push(change);
}
});
break;
case 'move':
break;
}
if (!found) {
failed.push(change);
}
});
return {
form: form,
failed: failed
};
}
/**
* Flatten the form components for data manipulation.
*
* @param {Object} components
* The components to iterate.
* @param {Boolean} includeAll
* Whether or not to include layout components.
*
* @returns {Object}
* The flattened components map.
*/
function flattenComponents(components, includeAll) {
var flattened = {};
eachComponent(components, function (component, path) {
flattened[path] = component;
}, includeAll);
return flattened;
}
/**
* Returns if this component has a conditional statement.
*
* @param component - The component JSON schema.
*
* @returns {boolean} - TRUE - This component has a conditional, FALSE - No conditional provided.
*/
function hasCondition(component) {
return Boolean(component.customConditional || component.conditional && (component.conditional.when || component.conditional.json || component.conditional.condition));
}
/**
* Extension of standard #parseFloat(value) function, that also clears input string.
*
* @param {any} value
* The value to parse.
*
* @returns {Number}
* Parsed value.
*/
function parseFloatExt(value) {
return parseFloat((0, _isString.default)(value) ? value.replace(/[^\de.+-]/gi, '') : value);
}
/**
* Formats provided value in way how Currency component uses it.
*
* @param {any} value
* The value to format.
*
* @returns {String}
* Value formatted for Currency component.
*/
function formatAsCurrency(value) {
var parsedValue = parseFloatExt(value);
if ((0, _isNaN.default)(parsedValue)) {
return '';
}
var parts = (0, _round.default)(parsedValue, 2).toString().split('.');
parts[0] = (0, _chunk.default)(Array.from(parts[0]).reverse(), 3).reverse().map(function (part) {
return part.reverse().join('');
}).join(',');
parts[1] = (0, _pad.default)(parts[1], 2, '0');
return parts.join('.');
}
/**
* Escapes RegEx characters in provided String value.
*
* @param {String} value
* String for escaping RegEx characters.
* @returns {string}
* String with escaped RegEx characters.
*/
function escapeRegExCharacters(value) {
return value.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
}
/**
* Get the value for a component key, in the given submission.
*
* @param {Object} submission
* A submission object to search.
* @param {String} key
* A for components API key to search for.
*/
function getValue(submission, key) {
var search = function search(data) {
if ((0, _isPlainObject.default)(data)) {
if ((0, _has.default)(data, key)) {
return _lodash.default.get(data, key);
}
var value = null;
(0, _forOwn.default)(data, function (prop) {
var result = search(prop);
if (!(0, _isNil.default)(result)) {
value = result;
return false;
}
});
return value;
} else {
return null;
}
};
return search(submission.data);
}
/**
* Iterate over all components in a form and get string values for translation.
* @param form
*/
function getStrings(form) {
var properties = ['label', 'title', 'legend', 'tooltip', 'description', 'placeholder', 'prefix', 'suffix', 'errorLabel', 'content', 'html'];
var strings = [];
eachComponent(form.components, function (component) {
properties.forEach(function (property) {
if (component.hasOwnProperty(property) && component[property]) {
strings.push({
key: component.key,
type: component.type,
property: property,
string: component[property]
});
}
});
if ((!component.dataSrc || component.dataSrc === 'values') && component.hasOwnProperty('values') && Array.isArray(component.values) && component.values.length) {
component.values.forEach(function (value, index) {
strings.push({
key: component.key,
property: "value[".concat(index, "].label"),
string: component.values[index].label
});
});
} // Hard coded values from Day component
if (component.type === 'day') {
['day', 'month', 'year', 'Day', 'Month', 'Year', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'].forEach(function (string) {
strings.push({
key: component.key,
property: 'day',
string: string
});
});
if (component.fields.day.placeholder) {
strings.push({
key: component.key,
property: 'fields.day.placeholder',
string: component.fields.day.placeholder
});
}
if (component.fields.month.placeholder) {
strings.push({
key: component.key,
property: 'fields.month.placeholder',
string: component.fields.month.placeholder
});
}
if (component.fields.year.placeholder) {
strings.push({
key: component.key,
property: 'fields.year.placeholder',
string: component.fields.year.placeholder
});
}
}
if (component.type === 'editgrid') {
var string = component.addAnother || 'Add Another';
if (component.addAnother) {
strings.push({
key: component.key,
property: 'addAnother',
string: string
});
}
}
if (component.type === 'select') {
['loading...', 'Type to search'].forEach(function (string) {
strings.push({
key: component.key,
property: 'select',
string: string
});
});
}
}, true);
return strings;
}