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,290 lines (1,221 loc) 157 kB
'use strict'; const filter = 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$1 = 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'); 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, (ChildNodeComponent, i) => { const key = ChildNodeComponent.key; return (jsxRuntime.jsxs("div", { style: { display: 'flex' }, children: [jsxRuntime.jsx(ChildNodeComponent, {}, key), !readOnly && (jsxRuntime.jsx(Button, { title: "remove item", label: "x", disabled: disabled, onClick: () => handleRemoveClick(i) }))] }, 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, fontSize: "1rem" })] }))] })); }; const Button = ({ title, label, disabled, onClick, size = '1.3rem', fontSize = '0.8rem', style, }) => { return (jsxRuntime.jsx("button", { title: title, onClick: onClick, disabled: disabled, style: { position: 'relative', width: size, height: size, padding: 0, border: 'none', cursor: 'pointer', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', ...style, }, children: jsxRuntime.jsx("span", { style: { fontSize, lineHeight: 1, position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, 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 ?? undefined, onChange: handleChange, style: style, className: className })); }; const FormTypeInputBooleanDefinition = { Component: FormTypeInputBoolean, test: { type: 'boolean' }, }; const FormTypeInputDateFormat = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, style, className, }) => { const { type, max, min } = react.useMemo(() => ({ type: jsonSchema.format, max: jsonSchema.options?.maximum, min: jsonSchema.options?.minimum, }), [jsonSchema]); const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); return (jsxRuntime.jsx("input", { type: type, id: path, name: name, readOnly: readOnly, disabled: disabled, max: max, min: min, defaultValue: defaultValue ?? undefined, onChange: handleChange, style: style, className: className })); }; const FormTypeInputDateFormatDefinition = { Component: FormTypeInputDateFormat, test: { type: 'string', format: ['month', 'week', 'date', 'time', 'datetime-local'], }, }; 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 ?? undefined, 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) => (jsxRuntime.jsx(ChildNodeComponent, {}, ChildNodeComponent.key))) : 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 type = react.useMemo(() => { if (jsonSchema?.format === 'password') return 'password'; else if (jsonSchema?.format === 'email') return 'email'; else return 'text'; }, [jsonSchema?.format]); const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); return (jsxRuntime.jsx("input", { type: type, id: path, name: name, readOnly: readOnly, disabled: disabled, placeholder: jsonSchema?.placeholder, defaultValue: defaultValue ?? undefined, 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, (rawValue) => { const value = '' + rawValue; return { value, rawValue, 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; const rawValue = checkboxOptions.find((option) => option.value === value)?.rawValue; if (rawValue === undefined) return; if (checked) onChange((prev) => [...(prev || []), rawValue]); else onChange((prev) => prev?.filter((option) => option !== rawValue) || []); }); return (jsxRuntime.jsx("div", { children: array.map(checkboxOptions, ({ value, rawValue, 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(rawValue), 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, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: context.enumLabels?.[value] || jsonSchema.options?.alias?.[value] || value, }; }) : [], [context, jsonSchema]); const initialValue = react.useMemo(() => { if (defaultValue === undefined) return undefined; if (defaultValue === null) return '' + null; return defaultValue; }, [defaultValue]); const handleChange = hook.useHandle((event) => { const rawValue = enumOptions.find((option) => option.value === event.target.value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); }); return (jsxRuntime.jsx("select", { id: path, name: name, disabled: disabled || readOnly, defaultValue: initialValue, 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, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: context.radioLabels?.[value] || jsonSchema.options?.alias?.[value] || value, }; }) : [], [context, jsonSchema]); const handleChange = hook.useHandle((event) => { const rawValue = radioOptions.find((option) => option.value === event.target.value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); }); return (jsxRuntime.jsx(react.Fragment, { children: array.map(radioOptions, ({ value, rawValue, 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: rawValue === 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 = [ FormTypeInputDateFormatDefinition, FormTypeInputStringCheckboxDefinition, FormTypeInputStringRadioDefinition, FormTypeInputStringEnumDefinition, FormTypeInputVirtualDefinition, FormTypeInputArrayDefinition, FormTypeInputObjectDefinition, FormTypeInputBooleanDefinition, FormTypeInputStringDefinition, FormTypeInputNumberDefinition, ]; const BIT_MASK_ALL = -1; const BIT_MASK_NONE = 0; const UNIT_SEPARATOR = '\x1F'; const START_OF_TEXT = '\x02'; const END_OF_TEXT = '\x03'; const ENHANCED_KEY = START_OF_TEXT + UNIT_SEPARATOR + END_OF_TEXT; const transformErrors = (errors, key) => { if (!filter.isArray(errors)) return []; const result = new Array(); for (let i = 0, l = errors.length; i < l; i++) { const error = errors[i]; if (error.dataPath.indexOf(ENHANCED_KEY) !== -1) continue; error.key = key ? ++sequence : undefined; result[result.length] = error; } return result; }; let sequence = 0; 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 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 JSONPointer = { Fragment: pointer.JSONPointer.Fragment, Separator: pointer.JSONPointer.Separator, Parent: '..', Current: '.', Index: '*', }; const isAbsolutePath = (pointer) => pointer[0] === JSONPointer.Separator || (pointer[0] === JSONPointer.Fragment && pointer[1] === JSONPointer.Separator); const joinSegment = (path, segment) => path ? path === JSONPointer.Separator ? path + segment : path + JSONPointer.Separator + segment : JSONPointer.Separator; const stripFragment = (path) => path[0] === JSONPointer.Fragment ? (path[1] ? path.slice(1) : JSONPointer.Separator) : path; const INCLUDE_INDEX_REGEX = new RegExp(`^(\\${JSONPointer.Fragment})?\\${JSONPointer.Separator}(?:.*\\${JSONPointer.Separator})?\\${JSONPointer.Index}(?:\\${JSONPointer.Separator}.*)?(?<!\\${JSONPointer.Separator})$`); const normalizeFormTypeInputMap = (formTypeInputMap) => { if (!formTypeInputMap) return []; const result = []; for (const [path, Component] of Object.entries(formTypeInputMap)) { if (!filter$1.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.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$1.isReactComponent(Component)) continue; if (filter.isFunction(test)) result.push({ test, Component: hoc.withErrorBoundary(Component), }); else if (filter.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)) { const subject = hint[key]; if (filter.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.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 && typeof schema.$ref === 'string') 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 processOneOfSchema = (schema, variant) => object$1.merge(schema, { properties: { [ENHANCED_KEY]: { const: variant, }, }, }); 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 processVirtualSchema = (schema) => { if (schema.virtual === undefined) return null; let expired = false; if (schema.required) { schema = transformCondition(schema, schema.virtual); expired = true; } if (schema.then) { schema.then = transformCondition(schema.then, schema.virtual); expired = true; } if (schema.else) { schema.else = transformCondition(schema.else, schema.virtual); expired = true; } return expired ? schema : null; }; const preprocessSchema = (schema) => scanner.scan(schema).getValue() || schema; const scanner = new scanner$1.JsonSchemaScanner({ options: { mutate: (entry) => { let schema = entry.schema; let idle = true; if (schema.type === 'object') { const processed = processVirtualSchema(schema); schema = processed || schema; idle = processed === null; } if (entry.keyword === 'oneOf') { schema = processOneOfSchema(schema, entry.variant); idle = false; } if (idle) return; return schema; }, }, }); 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["Initialized"] = 1] = "Initialized"; NodeEventType[NodeEventType["UpdatePath"] = 2] = "UpdatePath"; NodeEventType[NodeEventType["UpdateValue"] = 4] = "UpdateValue"; NodeEventType[NodeEventType["UpdateState"] = 8] = "UpdateState"; NodeEventType[NodeEventType["UpdateError"] = 16] = "UpdateError"; NodeEventType[NodeEventType["UpdateGlobalError"] = 32] = "UpdateGlobalError"; NodeEventType[NodeEventType["UpdateChildren"] = 64] = "UpdateChildren"; NodeEventType[NodeEventType["UpdateComputedProperties"] = 128] = "UpdateComputedProperties"; NodeEventType[NodeEventType["Focused"] = 256] = "Focused"; NodeEventType[NodeEventType["Blurred"] = 512] = "Blurred"; NodeEventType[NodeEventType["RequestFocus"] = 1024] = "RequestFocus"; NodeEventType[NodeEventType["RequestSelect"] = 2048] = "RequestSelect"; NodeEventType[NodeEventType["RequestRefresh"] = 4096] = "RequestRefresh"; NodeEventType[NodeEventType["RequestEmitChange"] = 8192] = "RequestEmitChange"; })(NodeEventType || (NodeEventType = {})); exports.NodeEventType = void 0; (function (PublicNodeEventType) { PublicNodeEventType[PublicNodeEventType["UpdateValue"] = 4] = "UpdateValue"; PublicNodeEventType[PublicNodeEventType["UpdateState"] = 8] = "UpdateState"; PublicNodeEventType[PublicNodeEventType["UpdateError"] = 16] = "UpdateError"; PublicNodeEventType[PublicNodeEventType["RequestFocus"] = 1024] = "RequestFocus"; PublicNodeEventType[PublicNodeEventType["RequestSelect"] = 2048] = "RequestSelect"; })(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["Batch"] = 16] = "Batch"; SetValueOption[SetValueOption["Isolate"] = 32] = "Isolate"; SetValueOption[SetValueOption["PublishUpdateEvent"] = 64] = "PublishUpdateEvent"; SetValueOption[SetValueOption["BatchedEmitChange"] = 18] = "BatchedEmitChange"; SetValueOption[SetValueOption["SoftReset"] = 23] = "SoftReset"; SetValueOption[SetValueOption["HardReset"] = 31] = "HardReset"; SetValueOption[SetValueOption["Default"] = 66] = "Default"; SetValueOption[SetValueOption["BatchDefault"] = 82] = "BatchDefault"; SetValueOption[SetValueOption["Merge"] = 126] = "Merge"; SetValueOption[SetValueOption["Overwrite"] = 127] = "Overwrite"; })(SetValueOption || (SetValueOption = {})); exports.SetValueOption = void 0; (function (PublicSetValueOption) { PublicSetValueOption[PublicSetValueOption["Merge"] = 126] = "Merge"; PublicSetValueOption[PublicSetValueOption["Overwrite"] = 127] = "Overwrite"; })(exports.SetValueOption || (exports.SetValueOption = {})); const getEmptyValue = (type) => { if (type === 'array') return []; if (type === 'object') return {}; return undefined; }; const getDefaultValue = (jsonSchema) => { if (jsonSchema.default !== undefined) return jsonSchema.default; if (jsonSchema.type === 'virtual') return []; return getEmptyValue(jsonSchema.type); }; 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; }; const afterMicrotask = (handler) => { let macrotaskId; const callback = () => { handler(); macrotaskId = undefined; }; return () => { if (macrotaskId) scheduler.cancelMacrotaskSafe(macrotaskId); macrotaskId = scheduler.scheduleMacrotaskSafe(callback); }; }; const NOT_PRECEDED_BY_IDENTIFIER_CHARACTER = `(?<![a-zA-Z0-9_#./])`; const SINGLE_PATH_SEGMENT = `(?:[^\\s\\(\\)/~]|~[01])+`; const MULTI_LEVEL_PATH_PATTERN = `${SINGLE_PATH_SEGMENT}(?:\\${JSONPointer.Separator}${SINGLE_PATH_SEGMENT})*`; const OPTIONAL_PATH_PATTERN = `(?:${MULTI_LEVEL_PATH_PATTERN})?(?!\\${JSONPointer.Separator})`; const MULTIPLE_PARENT_DIRECTORY_REFERENCES = `(?:\\${JSONPointer.Parent}\\${JSONPointer.Separator})+`; const SINGLE_CHARACTER_PREFIX_PATTERN = `(?:\\${JSONPointer.Fragment}|\\${JSONPointer.Current})`; const JSON_POINTER_PATH_REGEX = new RegExp(`${NOT_PRECEDED_BY_IDENTIFIER_CHARACTER}` + `(?:` + `${MULTIPLE_PARENT_DIRECTORY_REFERENCES}${OPTIONAL_PATH_PATTERN}` + `|` + `${SINGLE_CHARACTER_PREFIX_PATTERN}\\${JSONPointer.Separator}${OPTIONAL_PATH_PATTERN}` + `|` + `\\${JSONPointer.Separator}${MULTI_LEVEL_PATH_PATTERN}` + `|` + `\\(\\${JSONPointer.Separator}\\)` + `)`, 'g'); const ALIAS = '&'; const checkComputedOptionFactory = (jsonSchema, rootJsonSchema) => (pathManager, fieldName) => { const expression = rootJsonSchema[fieldName] ?? jsonSchema[fieldName] ?? jsonSchema.computed?.[fieldName] ?? jsonSchema[ALIAS + fieldName]; if (typeof expression === 'boolean') return () => expression; return createDynamicFunction(pathManager, fieldName, expression); }; const createDynamicFunction = (pathManager, fieldName, expression) => { if (typeof expression !== 'string') return; const computedExpression = expression .replace(JSON_POINTER_PATH_REGEX, (path) => { pathManager.set(path); return `dependencies[${pathManager.findIndex(path)}]`; }) .trim() .replace(/;$/, ''); if (computedExpression.length === 0) return; try { return new Function('dependencies', `return !!(${computedExpression})`); } catch (error) { throw new SchemaNodeError('COMPUTED_OPTION', `Failed to create dynamic function: ${fieldName} -> '${expression}'`, { fieldName, expression, computedExpression, error }); } }; const combineConditions = (conditions, operator = '&&') => { const filtered = conditions.filter(filter.isTruthy); if (filtered.length === 0) return null; if (filtered.length === 1) return filtered[0]; return array.map(filtered, (item) => '(' + item + ')').join(operator); }; const convertExpression = (condition, inverse = false, source = JSONPointer.Parent) => { const operations = []; for (const [key, value] of Object.entries(condition)) { if (filter.isArray(value)) { operations.push(`${inverse ? '!' : ''}${object$1.serializeNative(value)}.includes((${source}${JSONPointer.Separator}${key}))`); } else { if (typeof value === 'boolean') operations.push(`(${source}${JSONPointer.Separator}${key})${inverse ? '!==' : '==='}${value}`); else operations.push(`(${source}${JSONPointer.Separator}${key})${inverse ? '!==' : '==='}${object$1.serializeNative(value)}`); } } if (operations.length === 0) return null; if (operations.length === 1) return operations[0]; return operations.map((operation) => '(' + operation + ')').join('&&'); }; const getExpressionFromSchema = (schema) => { const properties = schema.properties; if (!filter.isPlainObject(properties)) return null; const condition = {}; for (const [key, subSchema] of Object.entries(properties)) { if (key === ENHANCED_KEY) continue; if (subSchema.type !== undefined || subSchema.$ref !== undefined) continue; if ('const' in subSchema) condition[key] = subSchema.const; else if ('enum' in subSchema && filter.isArray(subSchema.enum)) { if (subSchema.enum.length === 1) condition[key] = subSchema.enum[0]; else if (subSchema.enum.length > 1) condition[key] = subSchema.enum; } } return convertExpression(condition, false, JSONPointer.Current); }; const SIMPLE_EQUALITY_REGEX = /^\s*dependencies\[(\d+)\]\s*===\s*(['"])([^'"]+)\2\s*$/; const getConditionIndexFactory = (jsonSchema) => (pathManager, fieldName, conditionField) => { if (jsonSchema.type !== 'object') return undefined; const conditionSchemas = jsonSchema[fieldName]; if (!filter.isArray(conditionSchemas)) return undefined; const expressions = []; const schemaIndices = []; for (let i = 0, l = conditionSchemas.length; i < l; i++) { const oneOfSchema = conditionSchemas[i]; if (!oneOfSchema) continue; const condition = oneOfSchema.computed?.[conditionField] ?? oneOfSchema[ALIAS + conditionField]; if (typeof condition === 'boolean') { if (condition === true) { expressions.push('true'); schemaIndices.push(i); } continue; } const expression = combineConditions([ typeof condition === 'string' ? condition.trim() : null, getExpressionFromSchema(oneOfSchema), ]); if (expression === null) continue; expressions.push(expression .replace(JSON_POINTER_PATH_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 = Number(matches[1]); 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 = Number(keys[0]); 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]};`; try { return new Function('dependencies', `${lines.join('\n')}\nreturn -1;`); } catch (error) { throw new SchemaNodeError('CONDITION_INDEX', `Failed to create dynamic function: ${fieldName} -> '${expressions.join(', ')}'`, { fieldName, expressions, lines, error }); } }; const getObservedValuesFactory = (schema) => (pathManager, fieldName) => { const watch = schema?.computed?.[fieldName] ?? schema?.[ALIAS + fieldName]; if (!watch || !(filter.isString(watch) || filter.isArray(watch))) return; const watchValues = filter.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; try { 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;`); } catch (error) { throw new SchemaNodeError('OBSERVED_VALUES', `Failed to create dynamic function: ${fieldName} -> '${JSON.stringify(watch)}'`, { fieldName, watch, watchValueIndexes, error }); } }; 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(), active: checkComputedOption(pathManager, 'active'), visible: checkComputedOption(pathManager, 'visible'), readOnly: checkComputedOption(pathManager, 'readOnly'), disabled: checkComputedOption(pathManager, 'disabled'), oneOfIndex: getConditionIndex(pathManager, 'oneOf', 'if'), watchValues: getObservedValues(pathManager, 'watch'), }; }; class EventCascade { __currentBatch__ = null; __batchHandler__; constructor(batchHandler) { this.__batchHandler__ = batchHandler; } schedule(eventEntity) { const batch = this.__acquireBatch__(); batch.eventEntities.push(eventEntity); } __acquireBatch__() { const batch = this.__currentBatch__; if (batch && !batch.resolved) return batch; const nextBatch = { eventEntities: [] }; this.__currentBatch__ = nextBatch; scheduler.scheduleMicrotask(() => { nextBatch.resolved = true; this.__batchHandler__(mergeEvents(nextBatch.eventEntities)); }); return nextBatch; } } const mergeEvents = (eventEntities) => { const merged = { type: BIT_MASK_NONE, payload: {}, options: {}, }; for (let i = 0, l = eventEntities.length; i < l; i++) { const eventEntity = eventEntities[i]; merged.type |= eventEntity[0]; if (eventEntity[1] !== undefined) merged.payload[eventEntity[0]] = eventEntity[1]; if (eventEntity[2] !== undefined) merged.options[eventEntity[0]] = eventEntity[2]; } return merged; }; const getFallbackValidator = (error, jsonSchema) => () => [ { keyword: 'jsonSchemaCompileFailed', dataPath: JSONPointer.Separator, message: error.message, source: error, details: { jsonSchema, }, }, ]; const getNodeGroup = (schema) => { if (typeof schema.terminal === 'boolean') return schema.terminal ? 'terminal' : 'branch'; if (schema.type === 'array' || schema.type === 'object' || schema.type === 'virtual') return isTerminalFormTypeInput(schema) ? 'terminal' : 'branch'; return 'terminal'; }; const isTerminalFormTypeInput = (schema) => 'FormTypeInput' in schema && filter$1.isReactComponent(schema.FormTypeInput); const getNodeType = ({ type, }) => { if (type === 'number' || type === 'integer') return 'number'; else return type; }; const getSafeEmptyValue = (value, jsonSchema) => { if (value !== undefined) return value; return getEmptyValue(jsonSchema.type); }; const getScopedSegment = (name, scope, parentType, variant) => { if (!scope) return name; const index = variant !== undefined ? JSONPointer.Separator + variant : ''; if (scope === 'oneOf' || scope === 'anyOf' || scope === 'allOf') { switch (parentType) { case 'object': return scope + index + JSONPointer.Separator + 'properties' + JSONPointer.Separator + name; case 'array': return scope + index + JSONPointer.Separator + 'items'; default: return scope + index + JSONPointer.Separator + name; } } if (scope === 'properties') return 'properties' + JSONPointer.Separator + name; if (scope === 'items') return 'items' + index; if (variant !== undefined) return scope + JSONPointer.Separator + variant + JSONPointer.Separator + name; return scope + JSONPointer.Separator + name; }; const matchesSchemaPath = (source, target) => { const start = source[0] === JSONPointer.Fragment ? 1 : 0; if (source.indexOf(target, start) !== start) return false; const endCode = source[start + target.length]; return endCode === JSONPointer.Separator || endCode === undefined; }; const detectsCandidate = (source, candidate) => candidate.variant === undefined || candidate.variant === candidate.parentNode?.oneOfIndex || (source.variant === candidate.variant && source.parentNode === candidate.parentNode); const getSegments = (pointer) => pointer.split(JSONPointer.Separator).filter(filter.isTruthy); const traversal = (source, pointer) => { if (!source) return null; if (pointer === null) return source; const segments = typeof pointer === 'string' ? getSegments(pointer) : pointer; if (segments.length === 0) return source; let cursor = source; for (let i = 0, il = segments.length; i < il; 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 = source; } else { if (cursor.group === 'terminal') return null; const subnodes = cursor.subnodes; if (!subnodes?.length) return null; let tentative = true; let fallback = null; for (let j = 0, jl = subnodes.length; j < jl; j++) { const node = subnodes[j].node; if (node.escapedName !== segment) continue; if (fallback === null) fallback = node; if (detectsCandidate(source, node)) { tentative = false; cursor = node; break; } } if (tentative) if (fallback) cursor = fallback; else return null; if (cursor.group === 'terminal') return cursor; } } return cursor; }; const RECURSIVE_ERROR_OMITTED_KEYS = new Set(['key']); class AbstractNode { group; type; depth; isRoot; rootNode; parentNode; jsonSchema; scope; variant; required; nullable; #name; get name() { return this.#name; } #escapedName; get escapedName() { return this.#escapedName; } setName(name, actor) { if (actor !== this.parentNode && actor !== this) return; this.#name = name; this.#escapedName = pointer.escapeSegment(name); this.updatePath(); } #path; get path() { return this.#path; } #schemaPath; get schemaPath() { return this.#schemaPath; } get key() { return this.#schemaPath + UNIT_SEPARATOR + this.#path; } updatePath() { const previous = this.#path; const parent = this.parentNode; const escapedName = this.#escapedName; const current = joinSegment(parent?.path, escapedName); if (previous === current) return false; this.#path = current; this.#schemaPath = this.scope ? joinSegment(parent?.schemaPath, getScopedSegment(escapedName, this.scope, parent?.type, this.variant)) : joinSegment(parent?.schemaPath, escapedName); const subnodes = this.subnodes; if (subnodes?.length) for (const subnode of subnodes) subnode.node.updatePath(); this.publish(NodeEventType.UpdatePath, current, { previous, current }); return true; } #initialValue; #defaultValue; get defaultValue() { return this.#defaultValue; } setDefaultValue(value) { this.#initialValue = this.#defaultValue = value; } refresh(value) { this.#defaultValue = value; this.publish(NodeEventType.RequestRefresh); } setValue(input, option = SetValueOption.Overwrite) { const inputValue = typeof input === 'function' ? input(this.value) : input; this.applyValue(inputValue, option); } #handleChange; onChange(input, batch) { this.#handleChange(input, batch); } get children() { return null; } get subnodes() { return this.children; } constructor({ name, scope, variant, jsonSchema, defaultValue, onChange, parentNode, validationMode, validatorFactory, required, }) { this.type = getNodeType(jsonSchema); this.group = getNodeGroup(jsonSchema); this.scope = scope; this.variant = variant; this.jsonSchema = jsonSchema; this.parentNode = parentNode || null; this.required = required ?? false; this.nullable = jsonSchema.nullable || false; this.isRoot = !parentNode; this.rootNode = (parentNode?.rootNode || this); this.#name = name || ''; this.#escapedName = pointer.escapeSegment(this.#name); this.#path = joi