UNPKG

@canard/schema-form

Version:

React-based component library that renders forms based on JSON Schema with plugin system support for validators and UI components

1,305 lines (1,249 loc) 145 kB
'use strict'; const filter$1 = require('@winglet/common-utils/filter'); const object$1 = require('@winglet/common-utils/object'); const error = require('@winglet/common-utils/error'); const object = require('@winglet/react-utils/object'); const jsxRuntime = require('react/jsx-runtime'); const react = require('react'); const array = require('@winglet/common-utils/array'); const hook = require('@winglet/react-utils/hook'); const filter = require('@winglet/react-utils/filter'); const hoc = require('@winglet/react-utils/hoc'); const pointer = require('@winglet/json/pointer'); const _function = require('@winglet/common-utils/function'); const constant = require('@winglet/common-utils/constant'); const scanner$1 = require('@winglet/json-schema/scanner'); const scheduler = require('@winglet/common-utils/scheduler'); const lib = require('@winglet/common-utils/lib'); class SchemaFormError extends error.BaseError { constructor(code, message, details = {}) { super('SCHEMA_FORM_ERROR', code, message, details); this.name = 'SchemaFormError'; } } const isSchemaFormError = (error) => error instanceof SchemaFormError; class SchemaNodeError extends error.BaseError { constructor(code, message, details = {}) { super('SCHEMA_NODE_ERROR', code, message, details); this.name = 'SchemaNodeError'; } } const isSchemaNodeError = (error) => error instanceof SchemaNodeError; class UnhandledError extends error.BaseError { constructor(code, message, details = {}) { super('UNHANDLED_ERROR', code, message, details); this.name = 'UnhandledError'; } } const isUnhandledError = (error) => error instanceof UnhandledError; class ValidationError extends error.BaseError { constructor(code, message, details = {}) { super('VALIDATION_ERROR', code, message, details); this.name = 'ValidationError'; } } const isValidationError = (error) => error instanceof ValidationError; const FormInputRenderer = ({ Input }) => (jsxRuntime.jsx(Input, {})); const FormGroupRenderer = ({ node, depth, path, name, required, Input, errorMessage, style, className, }) => { if (depth === 0) return jsxRuntime.jsx(Input, {}); if (node.group === 'branch') { return (jsxRuntime.jsxs("fieldset", { style: { marginBottom: 5, marginLeft: 5 * depth, ...style, }, className: className, children: [jsxRuntime.jsx("legend", { children: node.name }), jsxRuntime.jsx("div", { children: jsxRuntime.jsx("em", { style: { fontSize: '0.85em' }, children: errorMessage }) }), jsxRuntime.jsx("div", { children: jsxRuntime.jsx(Input, {}) })] })); } else { return (jsxRuntime.jsxs("div", { style: { marginBottom: 5, marginLeft: 5 * depth, ...style, }, className: className, children: [node.parentNode?.type !== 'array' && (jsxRuntime.jsxs("label", { htmlFor: path, style: { marginRight: 5 }, children: [name, " ", required && jsxRuntime.jsx("span", { style: { color: 'red' }, children: "*" })] })), jsxRuntime.jsx(Input, {}), jsxRuntime.jsx("br", {}), jsxRuntime.jsx("em", { style: { fontSize: '0.85em' }, children: errorMessage })] })); } }; const FormLabelRenderer = ({ name }) => name; const FormErrorRenderer = ({ errorMessage }) => (jsxRuntime.jsx("em", { children: errorMessage })); const FormTypeInputArray = ({ node, readOnly, disabled, ChildNodeComponents, style, }) => { const handleClick = react.useCallback(() => { node.push(); }, [node]); const handleRemoveClick = react.useCallback((index) => { node.remove(index); }, [node]); return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 5, ...style }, children: [ChildNodeComponents && array.map(ChildNodeComponents, (ChildComponent, i) => (jsxRuntime.jsxs("div", { style: { display: 'flex' }, children: [jsxRuntime.jsx(ChildComponent, {}), !readOnly && (jsxRuntime.jsx(Button, { title: "remove item", label: "x", disabled: disabled, onClick: () => handleRemoveClick(i) }))] }, ChildComponent.key))), !readOnly && (jsxRuntime.jsxs("label", { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', cursor: 'pointer', marginBottom: 5, }, children: [jsxRuntime.jsx("div", { style: { marginRight: 10 }, children: "Add New Item" }), jsxRuntime.jsx(Button, { title: "add item", label: "+", disabled: disabled, onClick: handleClick, style: { fontSize: '1rem' } })] }))] })); }; function Button({ title, label, disabled, onClick, style, }) { return (jsxRuntime.jsx("button", { title: title, onClick: onClick, disabled: disabled, style: { width: '1.3rem', height: '1.3rem', fontSize: '0.8rem', fontWeight: 'normal', border: 'none', cursor: 'pointer', borderRadius: '50%', paddingInline: 'unset', paddingBlock: 'unset', ...style, }, children: label })); } const FormTypeInputArrayDefinition = { Component: FormTypeInputArray, test: { type: 'array' }, }; const FormTypeInputBoolean = ({ path, name, readOnly, disabled, defaultValue, onChange, style, className, }) => { const handleChange = hook.useHandle((event) => { onChange(event.target.checked); }); return (jsxRuntime.jsx("input", { type: "checkbox", id: path, name: name, disabled: disabled || readOnly, defaultChecked: !!defaultValue, onChange: handleChange, style: style, className: className })); }; const FormTypeInputBooleanDefinition = { Component: FormTypeInputBoolean, test: { type: 'boolean' }, }; const FormTypeInputDateFormant = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, style, className, }) => { const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); const { type, max, min } = react.useMemo(() => ({ type: jsonSchema.format, max: jsonSchema.options?.maximum, min: jsonSchema.options?.minimum, }), [jsonSchema]); return (jsxRuntime.jsx("input", { type: type, id: path, name: name, readOnly: readOnly, disabled: disabled, max: max, min: min, defaultValue: defaultValue, onChange: handleChange, style: style, className: className })); }; const FormTypeInputDateFormantDefinition = { Component: FormTypeInputDateFormant, test: ({ jsonSchema }) => jsonSchema.type === 'string' && ['month', 'week', 'date', 'time', 'datetime-local'].includes(jsonSchema.format), }; const FormTypeInputNumber = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, style, className, }) => { const handleChange = hook.useHandle((event) => { onChange(event.target.valueAsNumber); }); return (jsxRuntime.jsx("input", { type: "number", id: path, name: name, step: jsonSchema.multipleOf, readOnly: readOnly, disabled: disabled, placeholder: jsonSchema?.placeholder, defaultValue: defaultValue, onChange: handleChange, style: style, className: className })); }; const FormTypeInputNumberDefinition = { Component: FormTypeInputNumber, test: { type: ['number', 'integer'] }, }; const FormTypeInputObject = ({ ChildNodeComponents, }) => { const children = react.useMemo(() => { return ChildNodeComponents ? array.map(ChildNodeComponents, (ChildNodeComponent, index) => (jsxRuntime.jsx(ChildNodeComponent, {}, ChildNodeComponent.key || index))) : null; }, [ChildNodeComponents]); return jsxRuntime.jsx(react.Fragment, { children: children }); }; const FormTypeInputObjectDefinition = { Component: FormTypeInputObject, test: { type: 'object' }, }; const FormTypeInputString = ({ path, name, readOnly, disabled, jsonSchema, defaultValue, onChange, style, className, }) => { const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); const type = react.useMemo(() => { if (jsonSchema?.format === 'password') return 'password'; else if (jsonSchema?.format === 'email') return 'email'; else return 'text'; }, [jsonSchema?.format]); return (jsxRuntime.jsx("input", { type: type, id: path, name: name, readOnly: readOnly, disabled: disabled, placeholder: jsonSchema?.placeholder, defaultValue: defaultValue, onChange: handleChange, style: style, className: className })); }; const FormTypeInputStringDefinition = { Component: FormTypeInputString, test: { type: 'string' }, }; const FormTypeInputStringCheckbox = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, style, className, }) => { const checkboxOptions = react.useMemo(() => jsonSchema.items?.enum ? array.map(jsonSchema.items.enum, (value) => ({ value, label: context.checkboxLabels?.[value] || jsonSchema.options?.alias?.[value] || jsonSchema.items?.options?.alias?.[value] || value, })) : [], [context, jsonSchema]); const handleChange = hook.useHandle((event) => { const { value, checked } = event.target; if (checked) { onChange((prev) => [...(prev || []), value]); } else { onChange((prev) => prev?.filter((option) => option !== value) || []); } }); return (jsxRuntime.jsx("div", { children: array.map(checkboxOptions, ({ value, label }) => (jsxRuntime.jsxs("label", { style: style, className: className, children: [jsxRuntime.jsx("input", { type: "checkbox", id: path, name: name, readOnly: readOnly, disabled: disabled, value: value, defaultChecked: defaultValue?.includes(value), onChange: handleChange }), label] }, value))) })); }; const FormTypeInputStringCheckboxDefinition = { Component: FormTypeInputStringCheckbox, test: ({ jsonSchema }) => jsonSchema.type === 'array' && jsonSchema.formType === 'checkbox' && jsonSchema.items?.type === 'string' && !!jsonSchema.items?.enum?.length, }; const FormTypeInputStringEnum = ({ path, name, jsonSchema, defaultValue, onChange, readOnly, disabled, context, style, className, }) => { const enumOptions = react.useMemo(() => jsonSchema.enum ? array.map(jsonSchema.enum, (value) => ({ value, label: context.enumLabels?.[value] || jsonSchema.options?.alias?.[value] || value, })) : [], [context, jsonSchema]); const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); return (jsxRuntime.jsx("select", { id: path, name: name, disabled: disabled || readOnly, defaultValue: defaultValue, onChange: handleChange, style: style, className: className, children: array.map(enumOptions, ({ value, label }) => (jsxRuntime.jsx("option", { value: value, children: label }, value))) })); }; const FormTypeInputStringEnumDefinition = { Component: FormTypeInputStringEnum, test: ({ jsonSchema }) => jsonSchema.type === 'string' && !!jsonSchema.enum?.length, }; const FormTypeInputStringRadio = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, style, className, }) => { const radioOptions = react.useMemo(() => jsonSchema.enum ? array.map(jsonSchema.enum, (value) => ({ value, label: context.radioLabels?.[value] || jsonSchema.options?.alias?.[value] || value, })) : [], [context, jsonSchema]); const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); return (jsxRuntime.jsx(react.Fragment, { children: array.map(radioOptions, ({ value, label }) => (jsxRuntime.jsxs("label", { style: style, className: className, children: [jsxRuntime.jsx("input", { type: "radio", id: path, name: name, readOnly: readOnly, disabled: disabled, value: value, defaultChecked: value === defaultValue, onChange: handleChange }), label] }, value))) })); }; const FormTypeInputStringRadioDefinition = { Component: FormTypeInputStringRadio, test: ({ jsonSchema }) => jsonSchema.type === 'string' && (jsonSchema.formType === 'radio' || jsonSchema.formType === 'radiogroup') && !!jsonSchema.enum?.length, }; const FormTypeInputVirtual = ({ ChildNodeComponents, }) => { return (jsxRuntime.jsx(react.Fragment, { children: ChildNodeComponents && array.map(ChildNodeComponents, (ChildNodeComponent) => (jsxRuntime.jsx(ChildNodeComponent, {}, ChildNodeComponent.key))) })); }; const FormTypeInputVirtualDefinition = { Component: FormTypeInputVirtual, test: { type: 'virtual' }, }; const formTypeDefinitions = [ FormTypeInputDateFormantDefinition, FormTypeInputStringCheckboxDefinition, FormTypeInputStringRadioDefinition, FormTypeInputStringEnumDefinition, FormTypeInputVirtualDefinition, FormTypeInputArrayDefinition, FormTypeInputObjectDefinition, FormTypeInputBooleanDefinition, FormTypeInputStringDefinition, FormTypeInputNumberDefinition, ]; const formatError = (error, node, context) => { const errorMessages = node.jsonSchema.errorMessages; if (!errorMessages || !error.keyword) return error.message; const errorMessage = getErrorMessage(error.keyword, errorMessages, context); if (errorMessage) return replacePattern(errorMessage, error.details, node.value); return error.message; }; const getErrorMessage = (keyword, errorMessages, context) => { const errorMessage = errorMessages[keyword] || errorMessages.default; if (typeof errorMessage === 'string') return errorMessage; if (errorMessage && typeof errorMessage === 'object' && 'locale' in context) { const localeErrorMessage = errorMessage[context.locale]; if (typeof localeErrorMessage === 'string') return localeErrorMessage; } return null; }; const replacePattern = (errorMessage, details, value) => { let message = errorMessage; if (details && typeof details === 'object') for (const [key, value] of Object.entries(details)) message = message.replace('{' + key + '}', '' + value); message = message.replace('{value}', '' + value); return message; }; const JSONPointer = { Fragment: pointer.JSONPointer.Fragment, Separator: pointer.JSONPointer.Separator, Parent: '..', Current: '.', Index: '*', }; const JSON_POINTER_REGEX = new RegExp(`(?<![a-zA-Z0-9_])(\\${JSONPointer.Fragment}|\\${JSONPointer.Parent}|\\${JSONPointer.Current})?\\${JSONPointer.Separator}([a-zA-Z0-9]+(\\${JSONPointer.Separator}[a-zA-Z0-9]+)*)?`, 'g'); const INCLUDE_INDEX_REGEX = new RegExp(`^(\\${JSONPointer.Fragment})?\\${JSONPointer.Separator}(?:.*\\${JSONPointer.Separator})?\\${JSONPointer.Index}(?:\\${JSONPointer.Separator}.*)?(?<!\\${JSONPointer.Separator})$`); const isAbsolutePath = (pointer) => pointer[0] === '/' || (pointer[0] === '#' && pointer[1] === '/'); const joinSegment = (path, segment) => path ? (path === '/' ? path + segment : path + '/' + segment) : '/'; const stripFragment = (path) => path[0] === '#' ? (path[1] ? path.slice(1) : '/') : path; const normalizeFormTypeInputMap = (formTypeInputMap) => { if (!formTypeInputMap) return []; const result = []; for (const [path, Component] of Object.entries(formTypeInputMap)) { if (!filter.isReactComponent(Component)) continue; if (INCLUDE_INDEX_REGEX.test(path)) result.push({ test: formTypeTestFnFactory$1(path), Component: hoc.withErrorBoundary(Component), }); else result.push({ test: pathExactMatchFnFactory(path), Component: hoc.withErrorBoundary(Component), }); } return result; }; const pathExactMatchFnFactory = (inputPath) => { try { const path = stripFragment(inputPath); const regex = new RegExp(path); return (hint) => { if (hint.path === path) return true; if (regex.test(hint.path)) return true; return false; }; } catch (error) { throw new SchemaFormError('FORM_TYPE_INPUT_MAP', `FormTypeInputMap contains an invalid key pattern.: ${inputPath}`, { path: inputPath, error }); } }; const formTypeTestFnFactory$1 = (path) => { const segments = stripFragment(path).split(JSONPointer.Separator); return (hint) => { const hintSegments = hint.path.split(JSONPointer.Separator); if (segments.length !== hintSegments.length) return false; for (let i = 0, l = segments.length; i < l; i++) { const segment = segments[i]; const hintSegment = hintSegments[i]; if (segment === JSONPointer.Index) { if (!filter$1.isArrayIndex(hintSegment)) return false; } else if (segment !== hintSegment) return false; } return true; }; }; const normalizeFormTypeInputDefinitions = (formTypeInputDefinitions) => { if (!formTypeInputDefinitions) return []; const result = []; for (const { Component, test } of formTypeInputDefinitions) { if (!filter.isReactComponent(Component)) continue; if (filter$1.isFunction(test)) result.push({ test, Component: hoc.withErrorBoundary(Component), }); else if (filter$1.isPlainObject(test)) { result.push({ test: formTypeTestFnFactory(test), Component: hoc.withErrorBoundary(Component), }); } } return result; }; const formTypeTestFnFactory = (test) => { return (hint) => { for (const [key, reference] of Object.entries(test)) { if (!reference) continue; const subject = hint[key]; if (filter$1.isArray(reference)) { if (!reference.includes(subject)) return false; } else { if (reference !== subject) return false; } } return true; }; }; const defaultRenderKit = { FormGroup: FormGroupRenderer, FormLabel: FormLabelRenderer, FormInput: FormInputRenderer, FormError: FormErrorRenderer, }; const defaultFormTypeInputDefinitions = normalizeFormTypeInputDefinitions(formTypeDefinitions); const defaultFormatError = formatError; class PluginManager { static #renderKit = defaultRenderKit; static #formTypeInputDefinitions = defaultFormTypeInputDefinitions; static #validator; static #formatError = defaultFormatError; static get FormGroup() { return PluginManager.#renderKit.FormGroup; } static get FormLabel() { return PluginManager.#renderKit.FormLabel; } static get FormInput() { return PluginManager.#renderKit.FormInput; } static get FormError() { return PluginManager.#renderKit.FormError; } static get formatError() { return PluginManager.#formatError; } static get formTypeInputDefinitions() { return PluginManager.#formTypeInputDefinitions; } static get validator() { return PluginManager.#validator; } static reset() { PluginManager.#renderKit = defaultRenderKit; PluginManager.#formTypeInputDefinitions = defaultFormTypeInputDefinitions; PluginManager.#validator = undefined; PluginManager.#formatError = defaultFormatError; } static appendRenderKit(renderKit) { if (!renderKit) return; PluginManager.#renderKit = { ...PluginManager.#renderKit, ...object.remainOnlyReactComponent(renderKit), }; } static appendFormTypeInputDefinitions(formTypeInputDefinitions) { if (!formTypeInputDefinitions) return; PluginManager.#formTypeInputDefinitions = [ ...normalizeFormTypeInputDefinitions(formTypeInputDefinitions), ...PluginManager.#formTypeInputDefinitions, ]; } static appendValidator(validator) { if (!validator) return; PluginManager.#validator = validator; } static appendFormatError(formatError) { if (!formatError) return; PluginManager.#formatError = formatError; } } const RegisteredPlugin = new Set(); const registerPlugin = (plugin) => { if (plugin === null) PluginManager.reset(); if (!filter$1.isPlainObject(plugin)) return; const hash = object$1.stableSerialize(plugin); if (RegisteredPlugin.has(hash)) return; try { const { formTypeInputDefinitions, validator, formatError, ...renderKit } = plugin; PluginManager.appendRenderKit(renderKit); PluginManager.appendFormTypeInputDefinitions(formTypeInputDefinitions); PluginManager.appendValidator(validator); PluginManager.appendFormatError(formatError); } catch (error) { throw new UnhandledError('REGISTER_PLUGIN', 'Failed to register plugin', { plugin, error, }); } RegisteredPlugin.add(hash); }; const getReferenceTable = (jsonSchema) => { const referenceTable = new Map(); new scanner$1.JsonSchemaScanner({ visitor: { exit: ({ schema, hasReference }) => { if (hasReference && filter$1.isString(schema.$ref)) referenceTable.set(schema.$ref, pointer.getValue(jsonSchema, schema.$ref)); }, }, }).scan(jsonSchema); if (referenceTable.size === 0) return null; return referenceTable; }; const getResolveSchemaScanner = (referenceTable, maxDepth) => new scanner$1.JsonSchemaScanner({ options: { resolveReference: (path) => referenceTable.get(path), maxDepth, }, }); const getResolveSchema = (jsonSchema, maxDepth = 1) => { const table = getReferenceTable(jsonSchema); const scanner = table ? getResolveSchemaScanner(table, maxDepth) : null; return scanner ? (schema) => scanner.scan(schema).getValue() : null; }; const transformCondition = (schema, virtual) => { const transformed = Object.assign({}, schema); if (schema.required?.length) { const result = transformVirtualFields(schema.required, virtual); transformed.required = result.required; result.virtualRequired.length && (transformed.virtualRequired = result.virtualRequired); } schema.then && (transformed.then = transformCondition(schema.then, virtual)); schema.else && (transformed.else = transformCondition(schema.else, virtual)); return transformed; }; const transformVirtualFields = (required, virtual) => { const requiredKeys = []; const virtualRequiredKeys = []; for (let i = 0, il = required.length; i < il; i++) { const key = required[i]; const fields = virtual[key]?.fields; if (fields) { for (let j = 0, jl = fields.length; j < jl; j++) requiredKeys.indexOf(fields[j]) === -1 && requiredKeys.push(fields[j]); virtualRequiredKeys.indexOf(key) === -1 && virtualRequiredKeys.push(key); } else requiredKeys.indexOf(key) === -1 && requiredKeys.push(key); } return { required: requiredKeys, virtualRequired: virtualRequiredKeys }; }; const preprocessSchema = (schema) => scanner.scan(schema).getValue() || schema; const scanner = new scanner$1.JsonSchemaScanner({ options: { mutate: ({ schema }) => { if (schema.type !== 'object') return; if (!('virtual' in schema)) return; if ('required' in schema) schema = transformCondition(schema, schema.virtual); if (schema.then) schema.then = transformCondition(schema.then, schema.virtual); if (schema.else) schema.else = transformCondition(schema.else, schema.virtual); return schema; }, }, }); const BIT_MASK_ALL = -1; const BIT_MASK_NONE = 0; exports.ValidationMode = void 0; (function (ValidationMode) { ValidationMode[ValidationMode["None"] = 0] = "None"; ValidationMode[ValidationMode["OnChange"] = 1] = "OnChange"; ValidationMode[ValidationMode["OnRequest"] = 2] = "OnRequest"; })(exports.ValidationMode || (exports.ValidationMode = {})); var NodeEventType; (function (NodeEventType) { NodeEventType[NodeEventType["Activated"] = 1] = "Activated"; NodeEventType[NodeEventType["Focus"] = 2] = "Focus"; NodeEventType[NodeEventType["Select"] = 4] = "Select"; NodeEventType[NodeEventType["Redraw"] = 8] = "Redraw"; NodeEventType[NodeEventType["Refresh"] = 16] = "Refresh"; NodeEventType[NodeEventType["UpdatePath"] = 32] = "UpdatePath"; NodeEventType[NodeEventType["UpdateValue"] = 64] = "UpdateValue"; NodeEventType[NodeEventType["UpdateState"] = 128] = "UpdateState"; NodeEventType[NodeEventType["UpdateError"] = 256] = "UpdateError"; NodeEventType[NodeEventType["UpdateGlobalError"] = 512] = "UpdateGlobalError"; NodeEventType[NodeEventType["UpdateChildren"] = 1024] = "UpdateChildren"; NodeEventType[NodeEventType["UpdateComputedProperties"] = 2048] = "UpdateComputedProperties"; NodeEventType[NodeEventType["RequestEmitChange"] = 4096] = "RequestEmitChange"; NodeEventType[NodeEventType["RequestValidate"] = 8192] = "RequestValidate"; })(NodeEventType || (NodeEventType = {})); exports.NodeEventType = void 0; (function (PublicNodeEventType) { PublicNodeEventType[PublicNodeEventType["Focus"] = 2] = "Focus"; PublicNodeEventType[PublicNodeEventType["Select"] = 4] = "Select"; PublicNodeEventType[PublicNodeEventType["UpdateValue"] = 64] = "UpdateValue"; PublicNodeEventType[PublicNodeEventType["UpdateState"] = 128] = "UpdateState"; PublicNodeEventType[PublicNodeEventType["UpdateError"] = 256] = "UpdateError"; })(exports.NodeEventType || (exports.NodeEventType = {})); exports.NodeState = void 0; (function (NodeState) { NodeState[NodeState["Dirty"] = 1] = "Dirty"; NodeState[NodeState["Touched"] = 2] = "Touched"; NodeState[NodeState["ShowError"] = 4] = "ShowError"; })(exports.NodeState || (exports.NodeState = {})); var SetValueOption; (function (SetValueOption) { SetValueOption[SetValueOption["Replace"] = 1] = "Replace"; SetValueOption[SetValueOption["EmitChange"] = 2] = "EmitChange"; SetValueOption[SetValueOption["Propagate"] = 4] = "Propagate"; SetValueOption[SetValueOption["Refresh"] = 8] = "Refresh"; SetValueOption[SetValueOption["IsolationMode"] = 16] = "IsolationMode"; SetValueOption[SetValueOption["PublishUpdateEvent"] = 32] = "PublishUpdateEvent"; SetValueOption[SetValueOption["Default"] = 34] = "Default"; SetValueOption[SetValueOption["Merge"] = 62] = "Merge"; SetValueOption[SetValueOption["Overwrite"] = 63] = "Overwrite"; })(SetValueOption || (SetValueOption = {})); exports.SetValueOption = void 0; (function (PublicSetValueOption) { PublicSetValueOption[PublicSetValueOption["Merge"] = 62] = "Merge"; PublicSetValueOption[PublicSetValueOption["Overwrite"] = 63] = "Overwrite"; })(exports.SetValueOption || (exports.SetValueOption = {})); const getDefaultValue = (jsonSchema) => { if (jsonSchema.default !== undefined) return jsonSchema.default; else if (jsonSchema.type === 'array') return []; else if (jsonSchema.type === 'virtual') return []; else if (jsonSchema.type === 'object') return {}; else return undefined; }; const getObjectDefaultValue = (jsonSchema, inputDefault) => { const defaultValue = inputDefault || jsonSchema.default || {}; new scanner$1.JsonSchemaScanner({ visitor: { enter: ({ schema, dataPath }) => { if ('default' in schema) pointer.setValue(defaultValue, dataPath, schema.default, false); }, }, }).scan(jsonSchema); return defaultValue; }; let sequence = 0; const transformErrors = (errors, omits, key) => { if (!filter$1.isArray(errors)) return []; const result = new Array(); for (let i = 0, l = errors.length; i < l; i++) { const error = errors[i]; if (error.keyword && omits?.has(error.keyword)) continue; error.key = key ? ++sequence : undefined; result[result.length] = error; } return result; }; const afterMicrotask = (handler) => { let id; const callback = () => { handler(); id = undefined; }; return () => { if (id) scheduler.cancelMacrotask(id); id = scheduler.scheduleMacrotask(callback); }; }; const ALIAS = '&'; const checkComputedOptionFactory = (jsonSchema, rootJsonSchema) => (pathManager, fieldName, checkCondition) => { const expression = jsonSchema?.computed?.[fieldName] ?? jsonSchema?.[ALIAS + fieldName]; const preferredCondition = rootJsonSchema[fieldName] === checkCondition || jsonSchema[fieldName] === checkCondition || expression === checkCondition; return preferredCondition ? () => checkCondition : createDynamicFunction(pathManager, expression); }; const createDynamicFunction = (pathManager, expression) => { if (!filter$1.isString(expression)) return; const computedExpression = expression .replace(JSON_POINTER_REGEX, (path) => { pathManager.set(path); return `dependencies[${pathManager.findIndex(path)}]`; }) .trim() .replace(/;$/, ''); if (computedExpression.length === 0) return; return new Function('dependencies', `return !!(${computedExpression})`); }; const SIMPLE_EQUALITY_REGEX = /^\s*dependencies\[(\d+)\]\s*===\s*(['"])([^'"]+)\2\s*$/; const getConditionIndexFactory = (jsonSchema) => (pathManager, fieldName, conditionField) => { if (jsonSchema.type !== 'object' || !filter$1.isArray(jsonSchema[fieldName])) return undefined; const conditionSchemas = jsonSchema[fieldName]; const expressions = []; const schemaIndices = []; for (let i = 0, l = conditionSchemas.length; i < l; i++) { const condition = conditionSchemas[i]?.computed?.[conditionField] ?? conditionSchemas[i]?.[ALIAS + conditionField]; if (typeof condition === 'boolean') { if (condition === true) { expressions.push('true'); schemaIndices.push(i); } continue; } const expression = condition?.trim?.(); if (!expression || typeof expression !== 'string') continue; expressions.push(expression .replace(JSON_POINTER_REGEX, (path) => { pathManager.set(path); return `dependencies[${pathManager.findIndex(path)}]`; }) .replace(/;$/, '')); schemaIndices.push(i); } if (expressions.length === 0) return undefined; const equalityMap = {}; let isSimpleEquality = true; for (let i = 0, l = expressions.length; i < l; i++) { if (expressions[i] === 'true') { isSimpleEquality = false; break; } const matches = expressions[i].match(SIMPLE_EQUALITY_REGEX); if (matches) { const depIndex = parseInt(matches[1], 10); const value = matches[3]; if (!equalityMap[depIndex]) equalityMap[depIndex] = {}; if (!(value in equalityMap[depIndex])) equalityMap[depIndex][value] = schemaIndices[i]; } else { isSimpleEquality = false; break; } } const keys = Object.keys(equalityMap); if (isSimpleEquality && keys.length === 1) { const dependencyIndex = parseInt(keys[0], 10); const valueMap = equalityMap[dependencyIndex]; return (dependencies) => { const value = dependencies[dependencyIndex]; return typeof value === 'string' && value in valueMap ? valueMap[value] : -1; }; } const lines = new Array(expressions.length); for (let i = 0, l = expressions.length; i < l; i++) lines[i] = `if(${expressions[i]}) return ${schemaIndices[i]};`; return new Function('dependencies', `${lines.join('\n')} return -1; `); }; const getObservedValuesFactory = (schema) => (pathManager, fieldName) => { const watch = schema?.computed?.[fieldName] ?? schema?.[ALIAS + fieldName]; if (!watch || !(filter$1.isString(watch) || filter$1.isArray(watch))) return; const watchValues = filter$1.isArray(watch) ? watch : [watch]; const watchValueIndexes = []; for (let i = 0, l = watchValues.length; i < l; i++) { const path = watchValues[i]; pathManager.set(path); watchValueIndexes.push(pathManager.findIndex(path)); } if (watchValueIndexes.length === 0) return; return new Function('dependencies', `const indexes = [${watchValueIndexes.join(',')}]; const result = new Array(indexes.length); for (let i = 0, l = indexes.length; i < l; i++) result[i] = dependencies[indexes[i]]; return result;`); }; const getPathManager = () => { const paths = new Array(); return { get: () => paths, set: (path) => { if (path[0] === JSONPointer.Fragment) path = path.slice(1); if (!paths.includes(path)) paths.push(path); }, findIndex: (path) => { if (path[0] === JSONPointer.Fragment) path = path.slice(1); return paths.indexOf(path); }, }; }; const computeFactory = (schema, rootSchema) => { const checkComputedOption = checkComputedOptionFactory(schema, rootSchema); const getConditionIndex = getConditionIndexFactory(schema); const getObservedValues = getObservedValuesFactory(schema); const pathManager = getPathManager(); return { dependencyPaths: pathManager.get(), visible: checkComputedOption(pathManager, 'visible', false), readOnly: checkComputedOption(pathManager, 'readOnly', true), disabled: checkComputedOption(pathManager, 'disabled', true), oneOfIndex: getConditionIndex(pathManager, 'oneOf', 'if'), watchValues: getObservedValues(pathManager, 'watch'), }; }; class EventCascade { __currentBatch__ = null; __acquireBatch__() { const batch = this.__currentBatch__; if (batch && !batch.resolved) return batch; const nextBatch = { events: [] }; this.__currentBatch__ = nextBatch; scheduler.scheduleMicrotask(() => { nextBatch.resolved = true; this.__batchHandler__(mergeEvents(nextBatch.events)); }); return nextBatch; } __batchHandler__; constructor(batchHandler) { this.__batchHandler__ = batchHandler; } schedule(event) { const batch = this.__acquireBatch__(); batch.events.push(event); } } const mergeEvents = (events) => { const merged = { type: BIT_MASK_NONE, payload: {}, options: {}, }; for (const { type, payload, options } of events) { merged.type |= type; if (payload?.[type] !== undefined) merged.payload[type] = payload[type]; if (options?.[type] !== undefined) merged.options[type] = options[type]; } return merged; }; const find = (target, segments) => { if (!target) return null; if (!segments?.length) return target; const current = target; let cursor = current; for (let i = 0, l = segments.length; i < l; i++) { const segment = segments[i]; if (segment === JSONPointer.Fragment) { cursor = cursor.rootNode; if (!cursor) return null; } else if (segment === JSONPointer.Parent) { cursor = cursor.parentNode; if (!cursor) return null; } else if (segment === JSONPointer.Current) { cursor = current; } else { const children = cursor.children; if (!children?.length) return null; let found = false; for (const child of children) { if (child.node.propertyKey !== segment) continue; if (child.node.group === 'terminal') return child.node; cursor = child.node; found = true; break; } if (!found) return null; } } return cursor; }; const getPathSegments = (path) => { const segments = path.split(JSONPointer.Separator).filter(filter$1.isTruthy); if (segments.length === 0) return null; for (let i = 0, l = segments.length; i < l; i++) segments[i] = pointer.unescapeSegment(segments[i]); return segments; }; const getFallbackValidator = (error, jsonSchema) => () => [ { keyword: 'jsonSchemaCompileFailed', dataPath: JSONPointer.Separator, message: error.message, source: error, details: { jsonSchema, }, }, ]; const getNodeGroup = (schema) => { if (schema.type === 'boolean' || schema.type === 'number' || schema.type === 'integer' || schema.type === 'string' || schema.type === 'null' || schema.terminal === true || isTerminalFormTypeInput(schema)) return 'terminal'; return 'branch'; }; const isTerminalFormTypeInput = (schema) => 'FormType' in schema && filter.isReactComponent(schema.FormType) && schema.terminal !== false; const getNodeType = ({ type, }) => { if (type === 'number' || type === 'integer') return 'number'; else return type; }; const getSafeEmptyValue = (value, jsonSchema) => { if (value !== undefined) return value; else if (jsonSchema.type === 'array') return []; else if (jsonSchema.type === 'object') return {}; else return undefined; }; const IGNORE_ERROR_KEYWORDS = new Set(['oneOf']); const RECURSIVE_ERROR_OMITTED_KEYS = new Set(['key']); const RESET_NODE_OPTION$1 = SetValueOption.Replace | SetValueOption.Propagate; class AbstractNode { group; type; depth; isRoot; rootNode; parentNode; jsonSchema; propertyKey; escapedKey; required; #name; get name() { return this.#name; } setName(name, actor) { if (actor === this.parentNode || actor === this) { this.#name = name; this.updatePath(); } } #path; get path() { return this.#path; } updatePath() { const previous = this.#path; const parentPath = this.parentNode?.path; const current = joinSegment(parentPath, this.escapedKey); if (previous === current) return false; this.#path = current; this.publish({ type: NodeEventType.UpdatePath, payload: { [NodeEventType.UpdatePath]: current }, options: { [NodeEventType.UpdatePath]: { previous, current, }, }, }); return true; } #key; get key() { return this.#key; } #initialValue; #defaultValue; get defaultValue() { return this.#defaultValue; } setDefaultValue(value) { this.#initialValue = this.#defaultValue = value; } refresh(value) { this.#defaultValue = value; this.publish({ type: NodeEventType.Refresh }); } setValue(input, option = SetValueOption.Overwrite) { const inputValue = typeof input === 'function' ? input(this.value) : input; this.applyValue(inputValue, option); } #handleChange; onChange(input) { this.#handleChange?.(input); } get children() { return null; } constructor({ key, name, jsonSchema, defaultValue, onChange, parentNode, validationMode, validatorFactory, required, }) { this.type = getNodeType(jsonSchema); this.group = getNodeGroup(jsonSchema); this.jsonSchema = jsonSchema; this.parentNode = parentNode || null; this.required = required ?? false; this.rootNode = (this.parentNode?.rootNode || this); this.isRoot = !this.parentNode; this.#name = name || ''; this.propertyKey = this.#name; this.escapedKey = pointer.escapeSegment(this.propertyKey); this.#key = joinSegment(this.parentNode?.path, key ?? this.escapedKey); this.#path = joinSegment(this.parentNode?.path, this.escapedKey); this.depth = this.#path .split(JSONPointer.Separator) .filter(filter$1.isTruthy).length; if (this.parentNode) { const unsubscribe = this.parentNode.subscribe(({ type }) => { if (type & NodeEventType.UpdatePath) this.updatePath(); }); this.saveUnsubscribe(unsubscribe); } this.#compute = computeFactory(this.jsonSchema, this.rootNode.jsonSchema); this.setDefaultValue(defaultValue !== undefined ? defaultValue : getDefaultValue(jsonSchema)); if (typeof onChange === 'function') this.#handleChange = this.isRoot ? afterMicrotask(() => onChange(getSafeEmptyValue(this.value, this.jsonSchema))) : onChange; if (this.isRoot) this.#prepareValidator(validatorFactory, validationMode); } find(path) { if (path === undefined) return this; const useRootNode = isAbsolutePath(path); if (useRootNode && path.length === 1) return this.rootNode; return find(useRootNode ? this.rootNode : this, getPathSegments(path)); } #listeners = new Set(); #eventCascade = new EventCascade((event) => { for (const listener of this.#listeners) listener(event); }); #unsubscribes = []; saveUnsubscribe(unsubscribe) { this.#unsubscribes.push(unsubscribe); } #clearUnsubscribes() { for (let i = 0, l = this.#unsubscribes.length; i < l; i++) this.#unsubscribes[i](); this.#unsubscribes = []; } cleanUp(actor) { if (actor !== this.parentNode && !this.isRoot) return; this.#clearUnsubscribes(); this.#listeners.clear(); } subscribe(listener) { this.#listeners.add(listener); return () => { this.#listeners.delete(listener); }; } publish(event) { this.#eventCascade.schedule(event); } #activated = false; get activated() { return this.#activated; } activate(actor) { if (this.#activated || (actor !== this.parentNode && !this.isRoot)) return false; this.#activated = true; this.#prepareUpdateDependencies(); this.publish({ type: NodeEventType.Activated }); return true; } #compute; #dependencies = []; #visible = true; get visible() { return this.#visible; } #readOnly = false; get readOnly() { return this.#readOnly; } #disabled = false; get disabled() { return this.#disabled; } #oneOfIndex = -1; get oneOfIndex() { return this.#oneOfIndex; } #watchValues = []; get watchValues() { return this.#watchValues; } #prepareUpdateDependencies() { const dependencyPaths = this.#compute.dependencyPaths; if (dependencyPaths.length > 0) { this.#dependencies = new Array(dependencyPaths.length); for (let i = 0, l = dependencyPaths.length; i < l; i++) { const dependencyPath = dependencyPaths[i]; const targetNode = this.find(dependencyPath); if (!targetNode) continue; this.#dependencies[i] = targetNode.value; const unsubscribe = targetNode.subscribe(({ type, payload }) => { if (type & NodeEventType.UpdateValue) { if (this.#dependencies[i] !== payload?.[NodeEventType.UpdateValue]) { this.#dependencies[i] = payload?.[NodeEventType.UpdateValue]; this.updateComputedProperties(); } } }); this.saveUnsubscribe(unsubscribe); } } this.updateComputedProperties(); this.subscribe(({ type }) => { if (type & NodeEventType.UpdateComputedProperties) this.#hasPublishedUpdateComputedProperties = false; }); } #hasPublishedUpdateComputedProperties = false; updateComputedProperties() { const previousVisible = this.#visible; this.#visible = this.#compute.visible?.(this.#dependencies) ?? true; this.#readOnly = this.#compute.readOnly?.(this.#dependencies) ?? false; this.#disabled = this.#compute.disabled?.(this.#dependencies) ?? false; this.#watchValues = this.#compute.watchValues?.(this.#dependencies) || []; this.#oneOfIndex = this.#compute.oneOfIndex?.(this.#dependencies) ?? -1; if (previousVisible !== this.#visible) this.resetNode(true); if (!this.#hasPublishedUpdateComputedProperties) { this.publish({ type: NodeEventType.UpdateComputedProperties }); this.#hasPublishedUpdateComputedProperties = true; } } resetNode(preferLatest, input) { const defaultValue = preferLatest ? input !== undefined ? input : this.value !== undefined ? this.value : this.#initialValue : this.#initialValue; this.#defaultValue = defaultValue; const value = this.#visible ? defaultValue : undefined; this.setValue(value, RESET_NODE_OPTION$1); this.onChange(value); this.setState(); } #state = {}; get state() { return this.#state; } setState(input) { const newInput = typeof input === 'function' ? input(this.#state) : input; let dirty = false; if (newInput === undefined) { if (filter$1.isEmptyObject(this.#state)) return; this.#state = Object.create(null); dirty = true; } else if (filter$1.isObject(newInput)) { for (const [key, value] of Object.entries(newInput)) { if (value === undefined) { if (key in this.#state) { delete this.#state[key]; dirty = true; } } else if (this.#state[key] !== value) { this.#state[key] = value; dirty = true; } } } if (!dirty) return; this.publish({ type: NodeEventType.UpdateState, payload: { [NodeEventType.UpdateState]: this.#state }, }); } #externalErrors = []; #globalErrors; #errorDataPaths; #mergedGlobalErrors = []; #localErrors; #mergedLocalErrors = []; get globalErrors() { return this.isRoot ? this.#mergedGlobalErrors : this.rootNode.globalErrors; } get errors() { return this.#mergedLocalErrors; } setErrors(errors) { if (object$1.equals(this.#localErrors, errors)) return; this.#localErrors = errors; this.#mergedLocalErrors = [...this.#externalErrors, ...this.#localErrors]; this.publish({ type: NodeEventType.UpdateError, payload: { [NodeEventType.UpdateError]: this.#mergedLocalErrors }, }); } #setGlobalErrors(errors) { if (object$1.equals(this.#globalErrors, errors)) return false; this.#globalErrors = errors; this.#mergedGlobalErrors = [...this.#externalErrors, ...this.#globalErrors]; this.publish({ type: NodeEventType.UpdateGlobalError, payload: { [NodeEventType.UpdateGlobalError]: this.#mergedGlobalErrors }, }); return true; } clearErrors() { this.setErrors([]); } setExternalErrors(errors = []) { if (object$1.equals(this.#externalErrors, errors, RECURSIVE_ERROR_OMITTED_KEYS)) return; this.#externalErrors = new Array(errors.length); for (let i = 0, l = errors.length; i < l; i++) this.#externalErrors[i] = { ...errors[i], key: i }; this.#mergedLocalErrors = this.#localErrors ? [...this.#externalErrors, ...this.#localErrors] : this.#externalErrors; this.publish({ type: NodeEventType.UpdateError, payload: { [NodeEventType.UpdateError]: this.#mergedLocalErrors }, }); if (this.isRoot) { this.#mergedGlobalErrors = this.#globalErrors ? [...this.#externalErrors, ...this.#globalErrors] : this.#externalErrors; this.publish({