@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,314 lines (1,259 loc) • 143 kB
JavaScript
import { isArrayIndex, isFunction, isPlainObject, isArray, isString, isTruthy, isEmptyObject, isObject, isEmptyArray } from '@winglet/common-utils/filter';
import { stableSerialize, equals, merge, serializeNative, 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 { useCallback, useMemo, Fragment, createContext, useContext, useEffect, useRef, useState, memo, useLayoutEffect, forwardRef, useImperativeHandle, useSyncExternalStore } from 'react';
import { map, unique, sortWithReference } from '@winglet/common-utils/array';
import { useHandle, useConstant, useSnapshot, useVersion, useOnUnmount, useRestProperties, useMemorize } from '@winglet/react-utils/hook';
import { isReactComponent, isFunctionComponent, isMemoComponent } from '@winglet/react-utils/filter';
import { withErrorBoundary, withErrorBoundaryForwardRef } from '@winglet/react-utils/hoc';
import { JSONPointer as JSONPointer$1, getValue, setValue, unescapeSegment, escapeSegment } from '@winglet/json/pointer';
import { getTrackableHandler } from '@winglet/common-utils/function';
import { nullFunction, noopFunction } from '@winglet/common-utils/constant';
import { JsonSchemaScanner } from '@winglet/json-schema/scanner';
import { scheduleMacrotask, cancelMacrotask, scheduleMicrotask } from '@winglet/common-utils/scheduler';
import { getRandomString } from '@winglet/common-utils/lib';
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 SchemaNodeError extends BaseError {
constructor(code, message, details = {}) {
super('SCHEMA_NODE_ERROR', code, message, details);
this.name = 'SchemaNodeError';
}
}
const isSchemaNodeError = (error) => error instanceof SchemaNodeError;
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?.type !== 'array' && (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, (ChildComponent, i) => (jsxs("div", { style: { display: 'flex' }, children: [jsx(ChildComponent, {}), !readOnly && (jsx(Button, { title: "remove item", label: "x", disabled: disabled, onClick: () => handleRemoveClick(i) }))] }, ChildComponent.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, style: { fontSize: '1rem' } })] }))] }));
};
function Button({ title, label, disabled, onClick, style, }) {
return (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 = useHandle((event) => {
onChange(event.target.checked);
});
return (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 = useHandle((event) => {
onChange(event.target.value);
});
const { type, max, min } = useMemo(() => ({
type: jsonSchema.format,
max: jsonSchema.options?.maximum,
min: jsonSchema.options?.minimum,
}), [jsonSchema]);
return (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 = useHandle((event) => {
onChange(event.target.valueAsNumber);
});
return (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 = useMemo(() => {
return ChildNodeComponents
? map(ChildNodeComponents, (ChildNodeComponent, index) => (jsx(ChildNodeComponent, {}, ChildNodeComponent.key || index)))
: null;
}, [ChildNodeComponents]);
return jsx(Fragment, { children: children });
};
const FormTypeInputObjectDefinition = {
Component: FormTypeInputObject,
test: { type: 'object' },
};
const FormTypeInputString = ({ path, name, readOnly, disabled, jsonSchema, defaultValue, onChange, style, className, }) => {
const handleChange = useHandle((event) => {
onChange(event.target.value);
});
const type = useMemo(() => {
if (jsonSchema?.format === 'password')
return 'password';
else if (jsonSchema?.format === 'email')
return 'email';
else
return 'text';
}, [jsonSchema?.format]);
return (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 = useMemo(() => jsonSchema.items?.enum
? map(jsonSchema.items.enum, (value) => ({
value,
label: context.checkboxLabels?.[value] ||
jsonSchema.options?.alias?.[value] ||
jsonSchema.items?.options?.alias?.[value] ||
value,
}))
: [], [context, jsonSchema]);
const handleChange = useHandle((event) => {
const { value, checked } = event.target;
if (checked) {
onChange((prev) => [...(prev || []), value]);
}
else {
onChange((prev) => prev?.filter((option) => option !== value) || []);
}
});
return (jsx("div", { children: map(checkboxOptions, ({ value, 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(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 = useMemo(() => jsonSchema.enum
? map(jsonSchema.enum, (value) => ({
value,
label: context.enumLabels?.[value] ||
jsonSchema.options?.alias?.[value] ||
value,
}))
: [], [context, jsonSchema]);
const handleChange = useHandle((event) => {
onChange(event.target.value);
});
return (jsx("select", { id: path, name: name, disabled: disabled || readOnly, defaultValue: defaultValue, onChange: handleChange, style: style, className: className, children: map(enumOptions, ({ value, label }) => (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 = useMemo(() => jsonSchema.enum
? map(jsonSchema.enum, (value) => ({
value,
label: context.radioLabels?.[value] ||
jsonSchema.options?.alias?.[value] ||
value,
}))
: [], [context, jsonSchema]);
const handleChange = useHandle((event) => {
onChange(event.target.value);
});
return (jsx(Fragment, { children: map(radioOptions, ({ value, label }) => (jsxs("label", { style: style, className: className, children: [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 (jsx(Fragment, { children: ChildNodeComponents &&
map(ChildNodeComponents, (ChildNodeComponent) => (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: JSONPointer$1.Fragment,
Separator: JSONPointer$1.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 (!isReactComponent(Component))
continue;
if (INCLUDE_INDEX_REGEX.test(path))
result.push({
test: formTypeTestFnFactory$1(path),
Component: withErrorBoundary(Component),
});
else
result.push({
test: pathExactMatchFnFactory(path),
Component: 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 (!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) => {
return (hint) => {
for (const [key, reference] of Object.entries(test)) {
if (!reference)
continue;
const subject = hint[key];
if (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,
...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 getReferenceTable = (jsonSchema) => {
const referenceTable = new Map();
new JsonSchemaScanner({
visitor: {
exit: ({ schema, hasReference }) => {
if (hasReference && isString(schema.$ref))
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) => 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 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;
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["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 = {}));
var PublicNodeEventType;
(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";
})(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["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 = {}));
var PublicSetValueOption;
(function (PublicSetValueOption) {
PublicSetValueOption[PublicSetValueOption["Merge"] = 62] = "Merge";
PublicSetValueOption[PublicSetValueOption["Overwrite"] = 63] = "Overwrite";
})(PublicSetValueOption || (PublicSetValueOption = {}));
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 JsonSchemaScanner({
visitor: {
enter: ({ schema, dataPath }) => {
if ('default' in schema)
setValue(defaultValue, dataPath, schema.default, false);
},
},
}).scan(jsonSchema);
return defaultValue;
};
let sequence = 0;
const transformErrors = (errors, omits, 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.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)
cancelMacrotask(id);
id = 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 (!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' || !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 || !(isString(watch) || isArray(watch)))
return;
const watchValues = 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;
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(isTruthy);
if (segments.length === 0)
return null;
for (let i = 0, l = segments.length; i < l; i++)
segments[i] = 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 &&
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 = 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(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 (isEmptyObject(this.#state))
return;
this.#state = Object.create(null);
dirty = true;
}
else if (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 (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 (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 (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({
type: NodeEventType.UpdateGlobalError,
payload: {
[NodeEventType.UpdateGlobalError]: this.#mergedGlobalErrors,
},
});
}
}
clearExternalErrors() {
if (!this.#externalErrors.length)
return;
if (!this.isRoot)