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,291 lines (1,215 loc) 190 kB
import { isArray, isArrayIndex, isFunction, isPlainObject, isEmptyObject, isTruthy, isString, isObject, isEmptyArray } from '@winglet/common-utils/filter'; import { stableSerialize, merge, clone, equals, cloneLite, serializeNative, getEmptyObject, countKey, getFirstKey, getObjectKeys, sortObjectKeys } from '@winglet/common-utils/object'; import { BaseError } from '@winglet/common-utils/error'; import { remainOnlyReactComponent } from '@winglet/react-utils/object'; import { jsx, jsxs } from 'react/jsx-runtime'; import { isArraySchema, isStringSchema, isObjectSchema, isCompatibleSchemaType, isIdenticalSchemaType } from '@winglet/json-schema/filter'; import { useCallback, useMemo, Fragment, createContext, useContext, useEffect, useRef, useLayoutEffect, useState, memo, createElement, forwardRef, useImperativeHandle, useSyncExternalStore } from 'react'; import { map, intersectionWith, intersectionLite, unique, sortWithReference, primitiveArrayEqual, differenceLite } from '@winglet/common-utils/array'; import { useHandle, useConstant, useSnapshot, useMemorize, useVersion, useOnUnmount, useReference } from '@winglet/react-utils/hook'; import { isReactComponent, isMemoComponent } from '@winglet/react-utils/filter'; import { withErrorBoundary, withErrorBoundaryForwardRef } from '@winglet/react-utils/hoc'; import { JSONPointer as JSONPointer$1, getValue, setValue, escapeSegment } from '@winglet/json/pointer'; import { getTrackableHandler } from '@winglet/common-utils/function'; import { JsonSchemaScanner } from '@winglet/json-schema/scanner'; import { minLite, maxLite, lcm } from '@winglet/common-utils/math'; import { NOOP_FUNCTION, NULL_FUNCTION } from '@winglet/common-utils/constant'; import { hasOwnProperty } from '@winglet/common-utils/lib'; import { scheduleMacrotaskSafe, cancelMacrotaskSafe, scheduleMicrotask, scheduleMacrotask } from '@winglet/common-utils/scheduler'; class JsonSchemaError extends BaseError { constructor(code, message, details = {}) { super('JSON_SCHEMA_ERROR', code, message, details); this.name = 'JsonSchemaError'; } } const isJsonSchemaError = (error) => error instanceof JsonSchemaError; class SchemaFormError extends BaseError { constructor(code, message, details = {}) { super('SCHEMA_FORM_ERROR', code, message, details); this.name = 'SchemaFormError'; } } const isSchemaFormError = (error) => error instanceof SchemaFormError; class UnhandledError extends BaseError { constructor(code, message, details = {}) { super('UNHANDLED_ERROR', code, message, details); this.name = 'UnhandledError'; } } const isUnhandledError = (error) => error instanceof UnhandledError; class ValidationError extends BaseError { constructor(code, message, details = {}) { super('VALIDATION_ERROR', code, message, details); this.name = 'ValidationError'; } } const isValidationError = (error) => error instanceof ValidationError; const FormInputRenderer = ({ Input }) => (jsx(Input, {})); const FormGroupRenderer = ({ node, depth, path, name, required, Input, errorMessage, style, className, }) => { if (depth === 0) return jsx(Input, {}); if (node.group === 'branch') { return (jsxs("fieldset", { style: { marginBottom: 5, marginLeft: 5 * depth, ...style, }, className: className, children: [jsx("legend", { children: node.name }), jsx("div", { children: jsx("em", { style: { fontSize: '0.85em' }, children: errorMessage }) }), jsx("div", { children: jsx(Input, {}) })] })); } else { return (jsxs("div", { style: { marginBottom: 5, marginLeft: 5 * depth, ...style, }, className: className, children: [node.parentNode && isArraySchema(node.parentNode) === false && (jsxs("label", { htmlFor: path, style: { marginRight: 5 }, children: [name, " ", required && jsx("span", { style: { color: 'red' }, children: "*" })] })), jsx(Input, {}), jsx("br", {}), jsx("em", { style: { fontSize: '0.85em' }, children: errorMessage })] })); } }; const FormLabelRenderer = ({ name }) => name; const FormErrorRenderer = ({ errorMessage }) => (jsx("em", { children: errorMessage })); const FormTypeInputArray = ({ node, readOnly, disabled, ChildNodeComponents, style, }) => { const handleClick = useCallback(() => { node.push(); }, [node]); const handleRemoveClick = useCallback((index) => { node.remove(index); }, [node]); return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 5, ...style }, children: [ChildNodeComponents && map(ChildNodeComponents, (ChildNodeComponent, i) => { const key = ChildNodeComponent.key; return (jsxs("div", { style: { display: 'flex' }, children: [jsx(ChildNodeComponent, {}, key), !readOnly && (jsx(Button, { title: "remove item", label: "x", disabled: disabled, onClick: () => handleRemoveClick(i) }))] }, key)); }), !readOnly && (jsxs("label", { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', cursor: 'pointer', marginBottom: 5, }, children: [jsx("div", { style: { marginRight: 10 }, children: "Add New Item" }), 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 (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: 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 = useHandle((event) => { onChange(event.target.checked); }); return (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 } = useMemo(() => ({ type: jsonSchema.format, max: jsonSchema.options?.maximum, min: jsonSchema.options?.minimum, }), [jsonSchema]); const handleChange = useHandle((event) => { onChange(event.target.value); }); return (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, placeholder, style, className, }) => { const handleChange = useHandle((event) => { onChange(event.target.valueAsNumber); }); return (jsx("input", { type: "number", id: path, name: name, step: jsonSchema.multipleOf, readOnly: readOnly, disabled: disabled, placeholder: placeholder, defaultValue: defaultValue ?? undefined, onChange: handleChange, style: style, className: className })); }; const FormTypeInputNumberDefinition = { Component: FormTypeInputNumber, test: { type: ['number', 'integer'] }, }; const FormTypeInputObject = ({ ChildNodeComponents, }) => { const children = useMemo(() => { return ChildNodeComponents ? map(ChildNodeComponents, (ChildNodeComponent) => (jsx(ChildNodeComponent, {}, ChildNodeComponent.key))) : null; }, [ChildNodeComponents]); return jsx(Fragment, { children: children }); }; const FormTypeInputObjectDefinition = { Component: FormTypeInputObject, test: { type: 'object' }, }; const FormTypeInputString = ({ path, name, readOnly, disabled, jsonSchema, defaultValue, onChange, placeholder, style, className, }) => { const type = useMemo(() => { if (jsonSchema?.format === 'password') return 'password'; else if (jsonSchema?.format === 'email') return 'email'; else return 'text'; }, [jsonSchema?.format]); const handleChange = useHandle((event) => { onChange(event.target.value); }); return (jsx("input", { type: type, id: path, name: name, readOnly: readOnly, disabled: disabled, placeholder: 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, alias, style, className, }) => { const checkboxOptions = useMemo(() => jsonSchema.items?.enum ? map(jsonSchema.items.enum, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: context.checkboxLabels?.[value] || alias?.[value] || value, }; }) : [], [context, jsonSchema, alias]); const handleChange = 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 (jsx("div", { children: map(checkboxOptions, ({ value, rawValue, label }) => (jsxs("label", { style: style, className: className, children: [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: ({ type, jsonSchema }) => type === 'array' && jsonSchema.formType === 'checkbox' && isStringSchema(jsonSchema.items) && !!jsonSchema.items?.enum?.length, }; const FormTypeInputStringEnum = ({ path, name, jsonSchema, defaultValue, onChange, readOnly, disabled, context, alias, style, className, }) => { const enumOptions = useMemo(() => jsonSchema.enum ? map(jsonSchema.enum, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: context.enumLabels?.[value] || alias?.[value] || value, }; }) : [], [context, jsonSchema, alias]); const initialValue = useMemo(() => { if (defaultValue === undefined) return undefined; if (defaultValue === null) return '' + null; return defaultValue; }, [defaultValue]); const handleChange = useHandle((event) => { const rawValue = enumOptions.find((option) => option.value === event.target.value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); }); return (jsx("select", { id: path, name: name, disabled: disabled || readOnly, defaultValue: initialValue, onChange: handleChange, style: style, className: className, children: map(enumOptions, ({ value, label }) => (jsx("option", { value: value, children: label }, value))) })); }; const FormTypeInputStringEnumDefinition = { Component: FormTypeInputStringEnum, test: ({ type, jsonSchema }) => type === 'string' && !!jsonSchema.enum?.length, }; const FormTypeInputStringRadio = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, alias, style, className, }) => { const radioOptions = useMemo(() => jsonSchema.enum ? map(jsonSchema.enum, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: context.radioLabels?.[value] || alias?.[value] || value, }; }) : [], [context, jsonSchema, alias]); const handleChange = useHandle((event) => { const rawValue = radioOptions.find((option) => option.value === event.target.value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); }); return (jsx(Fragment, { children: map(radioOptions, ({ value, rawValue, label }) => (jsxs("label", { style: style, className: className, children: [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: ({ type, jsonSchema }) => type === 'string' && (jsonSchema.formType === 'radio' || jsonSchema.formType === 'radiogroup') && !!jsonSchema.enum?.length, }; const FormTypeInputVirtual = ({ ChildNodeComponents, }) => { return (jsx(Fragment, { children: ChildNodeComponents && map(ChildNodeComponents, (ChildNodeComponent) => (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 (!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' && context.locale !== undefined) { 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') { const keys = Object.keys(details); for (let i = 0, k = keys[0], l = keys.length; i < l; i++, k = keys[i]) message = message.replace('{' + k + '}', '' + details[k]); } 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 = { Root: JSONPointer$1.Root, Fragment: JSONPointer$1.Fragment, Separator: JSONPointer$1.Separator, Parent: '..', Current: '.', Index: '*', Context: '@', }; const isAbsolutePath = (pointer) => pointer[0] === JSONPointer.Separator || (pointer[0] === JSONPointer.Fragment && pointer[1] === JSONPointer.Separator); const joinSegment = (basePath = JSONPointer.Root, segment) => (segment !== '' ? basePath + JSONPointer.Separator + segment : basePath); const stripFragment = (path) => path[0] === JSONPointer.Fragment ? path[2] !== undefined ? path.slice(1) : JSONPointer.Root : path === JSONPointer.Separator ? JSONPointer.Root : 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 = []; const keys = Object.keys(formTypeInputMap); for (let i = 0, k = keys[0], l = keys.length; i < l; i++, k = keys[i]) { const Component = formTypeInputMap[k]; if (!isReactComponent(Component)) continue; if (INCLUDE_INDEX_REGEX.test(k)) result.push({ test: formTypeTestFnFactory$1(k), Component: withErrorBoundary(Component), }); else result.push({ test: pathExactMatchFnFactory(k), Component: withErrorBoundary(Component), }); } return result; }; const pathExactMatchFnFactory = (inputPath) => { try { const path = stripFragment(inputPath); const regex = path ? new RegExp(path) : null; 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 (!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 (!isReactComponent(Component)) continue; if (isFunction(test)) result.push({ test, Component: withErrorBoundary(Component), }); else if (isPlainObject(test)) { result.push({ test: formTypeTestFnFactory(test), Component: withErrorBoundary(Component), }); } } return result; }; const formTypeTestFnFactory = (test) => { const keys = Object.keys(test); return (hint) => { for (let i = 0, k = keys[0], l = keys.length; i < l; i++, k = keys[i]) { const reference = test[k]; const subject = hint[k]; if (isArray(reference)) { if (reference.indexOf(subject) === -1) 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, ...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 (!isPlainObject(plugin)) return; const hash = 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 extractSchemaInfo = (jsonSchema) => { if (jsonSchema === undefined) return null; const type = jsonSchema.type; if (type === undefined) return null; if (isArray(type)) { if (type.length === 0 || type.length > 2) return null; if (type.length === 1) return { type: type[0], nullable: type[0] === 'null' }; const nullIndex = type.indexOf('null'); if (nullIndex === -1) return null; return { type: type[nullIndex === 0 ? 1 : 0], nullable: true }; } return { type, nullable: type === 'null' || jsonSchema.nullable === true, }; }; const getReferenceTable = (jsonSchema) => { const referenceTable = new Map(); new JsonSchemaScanner({ visitor: { exit: ({ schema, hasReference }) => { if (hasReference && typeof schema.$ref === 'string') referenceTable.set(schema.$ref, getValue(jsonSchema, schema.$ref)); }, }, }).scan(jsonSchema); if (referenceTable.size === 0) return null; return referenceTable; }; const getResolveSchemaScanner = (referenceTable, maxDepth) => new JsonSchemaScanner({ options: { resolveReference: (path, entry) => { const { $ref: _, ...preferredSchema } = entry.schema; const referenceSchema = referenceTable.get(path); if (referenceSchema === undefined) return; if (isEmptyObject(preferredSchema)) return referenceSchema; return merge(clone(referenceSchema), preferredSchema); }, maxDepth, }, }); const getResolveSchema = (jsonSchema, maxDepth = 1) => { const table = getReferenceTable(jsonSchema); const scanner = table ? getResolveSchemaScanner(table, maxDepth) : null; return scanner ? (schema) => schema !== undefined ? scanner.scan(schema).getValue() : undefined : null; }; const processOneOfSchema = (schema, variant) => 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 JsonSchemaScanner({ options: { mutate: (entry) => { let schema = entry.schema; let idle = true; if (isObjectSchema(schema)) { const processed = processVirtualSchema(schema); schema = processed || schema; if (idle) idle = processed === null; } if (entry.keyword === 'oneOf') { schema = processOneOfSchema(schema, entry.variant); idle = false; } if (idle) return; return schema; }, }, }); const getCloneDepth = (schema) => isObjectSchema(schema) ? 3 : isArraySchema(schema) ? 2 : 1; const distributeAllOfProperties = (base, source) => { if (source.properties === undefined) return; if (base.properties === undefined) base.properties = source.properties; else { const properties = base.properties; const keys = Object.keys(source.properties); for (let i = 0, k = keys[0], l = keys.length; i < l; i++, k = keys[i]) { const subSchema = source.properties[k]; if (properties[k] === undefined) properties[k] = subSchema; else distributeSchema(properties[k], subSchema); } } }; const distributeAllOfItems = (base, source) => { if (base.items === false || source.items === undefined) return; else if (source.items === false) base.items = false; else if (base.items === undefined) base.items = source.items; else distributeSchema(base.items, source.items); }; const distributeSchema = (base, source) => { if (isArray(base.allOf)) base.allOf.push(source); else base.allOf = [source]; }; const intersectBooleanOr = (baseBool, sourceBool) => { if (baseBool === undefined && sourceBool === undefined) return undefined; if (baseBool === undefined) return sourceBool; if (sourceBool === undefined) return baseBool; return baseBool || sourceBool; }; const intersectConst = (baseConst, sourceConst) => { if (baseConst === undefined && sourceConst === undefined) return undefined; if (baseConst === undefined) return sourceConst; if (sourceConst === undefined) return baseConst; if (baseConst !== sourceConst) throw new JsonSchemaError('CONFLICTING_CONST_VALUES', `Conflicting const values: ${baseConst} vs ${sourceConst}`); return baseConst; }; const intersectEnum = (baseEnum, sourceEnum, deepEqual) => { if (!baseEnum && !sourceEnum) return undefined; if (!baseEnum) return sourceEnum; if (!sourceEnum) return baseEnum; const intersected = deepEqual ? intersectionWith(baseEnum, sourceEnum, equals) : intersectionLite(baseEnum, sourceEnum); if (intersected.length === 0) throw new JsonSchemaError('EMPTY_ENUM_INTERSECTION', 'Enum values must have at least one common value'); return intersected; }; const intersectMaximum = (baseMax, sourceMax) => { if (baseMax === undefined && sourceMax === undefined) return undefined; if (baseMax === undefined) return sourceMax; if (sourceMax === undefined) return baseMax; return minLite(baseMax, sourceMax); }; const intersectMinimum = (baseMin, sourceMin) => { if (baseMin === undefined && sourceMin === undefined) return undefined; if (baseMin === undefined) return sourceMin; if (sourceMin === undefined) return baseMin; return maxLite(baseMin, sourceMin); }; const FIRST_WIN_FIELDS = [ 'title', 'description', '$comment', 'examples', 'default', 'readOnly', 'writeOnly', 'format', 'additionalProperties', 'patternProperties', 'prefixItems', ]; const SPECIAL_FIELDS = [ 'type', 'enum', 'const', 'required', 'nullable', 'pattern', 'multipleOf', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'minProperties', 'maxProperties', 'minContains', 'maxContains', 'uniqueItems', 'propertyNames', 'properties', 'items', ]; const IGNORE_FIELDS = [ 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependencies', 'dependentRequired', 'dependentSchemas', 'unevaluatedProperties', 'unevaluatedItems', 'contains', ]; const EXCLUDE_FIELDS = new Set([ ...FIRST_WIN_FIELDS, ...SPECIAL_FIELDS, ...IGNORE_FIELDS, ]); const processFirstWinFields = (base, source) => { for (let i = 0, l = FIRST_WIN_FIELDS.length; i < l; i++) { const field = FIRST_WIN_FIELDS[i]; const baseValue = base[field]; const sourceValue = source[field]; if (baseValue !== undefined) base[field] = baseValue; else if (sourceValue !== undefined) base[field] = sourceValue; } }; const processOverwriteFields = (base, source) => { const keys = Object.keys(source); for (let i = 0, k = keys[0], l = keys.length; i < l; i++, k = keys[i]) { const value = source[k]; if (EXCLUDE_FIELDS.has(k) || value === undefined) continue; base[k] = value; } }; const processSchemaType = (base, source) => { const baseInfo = extractSchemaInfo(base); if (baseInfo === null) return; const sourceInfo = extractSchemaInfo(source); const sourceNullable = (sourceInfo?.nullable ?? source.nullable) !== false; const schemaType = sourceInfo !== null ? intersectSchemaType(baseInfo.type, sourceInfo.type) : baseInfo.type; if (base.nullable !== undefined) base.nullable = undefined; if (baseInfo.nullable && sourceNullable) base.type = (schemaType !== 'null' ? [schemaType, 'null'] : 'null'); else base.type = schemaType; }; const intersectSchemaType = (baseType, sourceType) => (baseType === 'number' && sourceType === 'integer' ? 'integer' : baseType); const unionRequired = (baseRequired, sourceRequired) => { if (!baseRequired && !sourceRequired) return undefined; if (!baseRequired) return sourceRequired; if (!sourceRequired) return baseRequired; return unique([...baseRequired, ...sourceRequired]); }; const validateRange = (min, max, errorMessage = 'Invalid range: min > max') => { if (min !== undefined && max !== undefined && min > max) { throw new JsonSchemaError('INVALID_RANGE', `${errorMessage} (${min} > ${max})`); } }; const intersectArraySchema = (base, source) => { processSchemaType(base, source); processFirstWinFields(base, source); processOverwriteFields(base, source); distributeAllOfItems(base, source); const enumResult = intersectEnum(base.enum, source.enum, true); const constResult = intersectConst(base.const, source.const); const requiredResult = unionRequired(base.required, source.required); const minItems = intersectMinimum(base.minItems, source.minItems); const maxItems = intersectMaximum(base.maxItems, source.maxItems); const minContains = intersectMinimum(base.minContains, source.minContains); const maxContains = intersectMaximum(base.maxContains, source.maxContains); const uniqueItems = intersectBooleanOr(base.uniqueItems, source.uniqueItems); validateRange(minItems, maxItems, 'Invalid array constraints: minItems'); validateRange(minContains, maxContains, 'Invalid array constraints: minContains'); if (enumResult !== undefined) base.enum = enumResult; if (constResult !== undefined) base.const = constResult; if (requiredResult !== undefined) base.required = requiredResult; if (minItems !== undefined) base.minItems = minItems; if (maxItems !== undefined) base.maxItems = maxItems; if (minContains !== undefined) base.minContains = minContains; if (maxContains !== undefined) base.maxContains = maxContains; if (uniqueItems !== undefined) base.uniqueItems = uniqueItems; return base; }; const intersectBooleanSchema = (base, source) => { processSchemaType(base, source); processFirstWinFields(base, source); processOverwriteFields(base, source); const enumResult = intersectEnum(base.enum, source.enum); const constResult = intersectConst(base.const, source.const); const requiredResult = unionRequired(base.required, source.required); if (enumResult !== undefined) base.enum = enumResult; if (constResult !== undefined) base.const = constResult; if (requiredResult !== undefined) base.required = requiredResult; return base; }; const intersectNullSchema = (base, source) => { processSchemaType(base, source); processFirstWinFields(base, source); processOverwriteFields(base, source); const enumResult = intersectEnum(base.enum, source.enum); const constResult = intersectConst(base.const, source.const); const requiredResult = unionRequired(base.required, source.required); if (enumResult !== undefined) base.enum = enumResult; if (constResult !== undefined) base.const = constResult; if (requiredResult !== undefined) base.required = requiredResult; return base; }; const intersectMultipleOf = (baseMultiple, sourceMultiple) => { if (baseMultiple === undefined && sourceMultiple === undefined) return undefined; if (baseMultiple === undefined) return sourceMultiple; if (sourceMultiple === undefined) return baseMultiple; return lcm(baseMultiple, sourceMultiple); }; const intersectNumberSchema = (base, source) => { processSchemaType(base, source); processFirstWinFields(base, source); processOverwriteFields(base, source); const enumResult = intersectEnum(base.enum, source.enum); const constResult = intersectConst(base.const, source.const); const requiredResult = unionRequired(base.required, source.required); const minimum = intersectMinimum(base.minimum, source.minimum); const maximum = intersectMaximum(base.maximum, source.maximum); const exclusiveMinimum = intersectMinimum(base.exclusiveMinimum, source.exclusiveMinimum); const exclusiveMaximum = intersectMaximum(base.exclusiveMaximum, source.exclusiveMaximum); const multipleOf = intersectMultipleOf(base.multipleOf, source.multipleOf); validateRange(minimum, maximum, 'Invalid number constraints: minimum'); validateRange(exclusiveMinimum, exclusiveMaximum, 'Invalid number constraints: exclusiveMinimum'); if (enumResult !== undefined) base.enum = enumResult; if (constResult !== undefined) base.const = constResult; if (requiredResult !== undefined) base.required = requiredResult; if (minimum !== undefined) base.minimum = minimum; if (maximum !== undefined) base.maximum = maximum; if (exclusiveMinimum !== undefined) base.exclusiveMinimum = exclusiveMinimum; if (exclusiveMaximum !== undefined) base.exclusiveMaximum = exclusiveMaximum; if (multipleOf !== undefined) base.multipleOf = multipleOf; return base; }; const intersectPattern = (basePattern, sourcePattern) => { if (!basePattern && !sourcePattern) return undefined; if (!basePattern) return sourcePattern; if (!sourcePattern) return basePattern; return '(?=' + basePattern + ')(?=' + sourcePattern + ')'; }; const intersectStringSchema = (base, source) => { processSchemaType(base, source); processFirstWinFields(base, source); processOverwriteFields(base, source); const enumResult = intersectEnum(base.enum, source.enum); const constResult = intersectConst(base.const, source.const); const requiredResult = unionRequired(base.required, source.required); const pattern = intersectPattern(base.pattern, source.pattern); const minLength = intersectMinimum(base.minLength, source.minLength); const maxLength = intersectMaximum(base.maxLength, source.maxLength); validateRange(minLength, maxLength, 'Invalid string constraints: minLength'); if (enumResult !== undefined) base.enum = enumResult; if (constResult !== undefined) base.const = constResult; if (requiredResult !== undefined) base.required = requiredResult; if (pattern !== undefined) base.pattern = pattern; if (minLength !== undefined) base.minLength = minLength; if (maxLength !== undefined) base.maxLength = maxLength; return base; }; const intersectObjectSchema = (base, source) => { processSchemaType(base, source); processFirstWinFields(base, source); processOverwriteFields(base, source); distributeAllOfProperties(base, source); const enumResult = intersectEnum(base.enum, source.enum, true); const constResult = intersectConst(base.const, source.const); const requiredResult = unionRequired(base.required, source.required); const propertyNames = base.propertyNames && source.propertyNames ? intersectStringSchema(base.propertyNames, source.propertyNames) : base.propertyNames || source.propertyNames; const minProperties = intersectMinimum(base.minProperties, source.minProperties); const maxProperties = intersectMaximum(base.maxProperties, source.maxProperties); validateRange(minProperties, maxProperties, 'Invalid object constraints: minProperties'); if (enumResult !== undefined) base.enum = enumResult; if (constResult !== undefined) base.const = constResult; if (requiredResult !== undefined) base.required = requiredResult; if (propertyNames !== undefined) base.propertyNames = propertyNames; if (minProperties !== undefined) base.minProperties = minProperties; if (maxProperties !== undefined) base.maxProperties = maxProperties; return base; }; const getMergeSchemaHandler = (schema) => { const schemaInfo = extractSchemaInfo(schema); switch (schemaInfo?.type) { case 'array': return intersectArraySchema; case 'boolean': return intersectBooleanSchema; case 'null': return intersectNullSchema; case 'number': case 'integer': return intersectNumberSchema; case 'object': return intersectObjectSchema; case 'string': return intersectStringSchema; } return null; }; const validateCompatibility = (schema, allOfSchema) => allOfSchema.type === undefined || isCompatibleSchemaType(schema, allOfSchema); const processAllOfSchema = (schema) => { if (!schema.allOf?.length) return schema; const mergeHandler = getMergeSchemaHandler(schema); if (!mergeHandler) return schema; const { allOf, ...rest } = schema; schema = cloneLite(rest, getCloneDepth(schema)); for (let i = 0, l = allOf.length; i < l; i++) { const allOfSchema = allOf[i]; if (validateCompatibility(schema, allOfSchema) === false) throw new JsonSchemaError('ALL_OF_TYPE_REDEFINITION', 'Type cannot be redefined in allOf schema. It must either be omitted or match the parent schema type.', { schema, allOfSchema }); schema = mergeHandler(schema, allOfSchema); } return schema; }; const stripSchemaExtensions = (jsonSchema) => new JsonSchemaScanner({ options: { mutate } }).scan(jsonSchema).getValue() || jsonSchema; const mutate = ({ schema, }) => { if (schema == null) return; if (schema.FormTypeInput === undefined && schema.FormTypeInputProps === undefined && schema.FormTypeRendererProps === undefined && schema.errorMessages === undefined && schema.options === undefined) return; const { FormTypeInput, FormTypeInputProps, FormTypeRendererProps, errorMessages, options, ...stripedSchema } = schema; return stripedSchema; }; const isTerminalType = (type) => type === 'boolean' || type === 'number' || type === 'integer' || type === 'string' || type === 'null'; const isBranchType = (type) => type === 'array' || type === 'object' || type === 'virtual'; var ValidationMode; (function (ValidationMode) { ValidationMode[ValidationMode["None"] = 0] = "None"; ValidationMode[ValidationMode["OnChange"] = 1] = "OnChange"; ValidationMode[ValidationMode["OnRequest"] = 2] = "OnRequest"; })(ValidationMode || (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 = {})); var PublicNodeEventType; (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"; })(PublicNodeEventType || (PublicNodeEventType = {})); var NodeState; (function (NodeState) { NodeState[NodeState["Dirty"] = 1] = "Dirty"; NodeState[NodeState["Touched"] = 2] = "Touched"; NodeState[NodeState["ShowError"] = 4] = "ShowError"; })(NodeState || (NodeState = {})); var SetValueOption; (function (SetValueOption) { SetValueOption[SetValueOption["None"] = 0] = "None"; 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["Normalize"] = 64] = "Normalize"; SetValueOption[SetValueOption["PublishUpdateEvent"] = 128] = "PublishUpdateEvent"; SetValueOption[SetValueOption["BatchedEmitChange"] = 18] = "BatchedEmitChange"; SetValueOption[SetValueOption["Default"] = 130] = "Default"; SetValueOption[SetValueOption["BatchDefault"] = 146] = "BatchDefault"; SetValueOption[SetValueOption["Reset"] = 151] = "Reset"; SetValueOption[SetValueOption["IsolateReset"] = 183] = "IsolateReset"; SetValueOption[SetValueOption["StableReset"] = 223] = "StableReset"; SetValueOption[SetValueOption["Merge"] = 190] = "Merge"; SetValueOption[SetValueOption["Overwrite"] = 191] = "Overwrite"; })(SetValueOption || (SetValueOption = {})); var PublicSetValueOption; (function (PublicSetValueOption) { PublicSetValueOption[PublicSetValueOption["Merge"] = 190] = "Merge"; PublicSetValueOption[PublicSetValueOption["Overwrite"] = 191] = "Overwrite"; })(PublicSetValueOption || (PublicSetValueOption = {})); 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 []; const schemaInfo = extractSchemaInfo(jsonSchema); if (schemaInfo === null) return undefined; return getEmptyValue(schemaInfo.type); }; const getObjectDefaultValue = (jsonSchema, inputDefault) => { const defaultValue = inputDefault !== undefined ? inputDefault : jsonSchema.default; const result = defaultValue || {}; new JsonSchemaScanner({ visitor: { enter: ({ schema, dataPath }) => { if (hasOwnProperty(schema, 'default')) setValue(result, dataPath, schema.default, SET_VALUE_OPTIONS); }, }, }).scan(jsonSchema); if (isEmptyObject(result)) return defaultValue; return result; }; const SET_VALUE_OPTIONS = { overwrite: false, preserveNull: false, }; const afterMicrotask = (handler) => { let macrotaskId; const callback = () => { handler(); macrotaskId = undefined; }; return () => { if (macrotaskId) cancelMacrotaskSafe(macrotaskId); macrotaskId = scheduleMacrotaskSafe(callback); }; }; const checkDefinedValue = (value) => { if (value === null) return true; if (typeof value === 'object') { for (const key in value) if (hasOwnProperty(value, key)) return true; return false; } return value !== undefined; }; const IDENTIFIER_CHARS = 'a-zA-Z0-9_'; const PATH_PREFIX_CHARS = `