@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
JavaScript
'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