From 1933c895a4a47b7f385909074c657e69b0dd2151 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Tue, 7 Nov 2023 19:32:08 +0300 Subject: [PATCH 01/33] Field itself is the controller --- README.md | 8 +- packages/react/src/main/AccessorContext.ts | 2 +- packages/reset-plugin/src/main/resetPlugin.ts | 2 +- packages/roqueform/src/main/composePlugins.ts | 54 ++-- packages/roqueform/src/main/createField.ts | 215 ++++++++-------- .../roqueform/src/main/naturalAccessor.ts | 4 +- packages/roqueform/src/main/shared-types.ts | 147 ----------- packages/roqueform/src/main/typings.ts | 243 ++++++++++++++++++ packages/roqueform/src/main/utils.ts | 63 +---- .../roqueform/src/main/validationPlugin.ts | 189 ++++++++------ packages/zod-plugin/src/main/zodPlugin.ts | 2 +- 11 files changed, 493 insertions(+), 436 deletions(-) delete mode 100644 packages/roqueform/src/main/shared-types.ts create mode 100644 packages/roqueform/src/main/typings.ts diff --git a/README.md b/README.md index a3117a3f..a03a9339 100644 --- a/README.md +++ b/README.md @@ -282,20 +282,20 @@ planetsField.at(1).value; # Accessors -[`Accessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates +[`ValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates field values. - When the new field is derived via [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#at) method, the field value is read from the value of the parent field using the - [`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. + [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. - When a field value is updated via [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue), then the parent field value is updated with the value returned from the - [`Accessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the + [`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the updated field has derived fields, their values are updated with values returned from the - [`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. + [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. You can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses [`naturalAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalAccessor.html): diff --git a/packages/react/src/main/AccessorContext.ts b/packages/react/src/main/AccessorContext.ts index 0d2a0625..2a271a40 100644 --- a/packages/react/src/main/AccessorContext.ts +++ b/packages/react/src/main/AccessorContext.ts @@ -1,5 +1,5 @@ import { createContext, Context } from 'react'; -import { naturalAccessor, Accessor } from 'roqueform'; +import { naturalAccessor, ValueAccessor } from 'roqueform'; /** * The context that is used by {@link useField} to retrieve an accessor. diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 416bb88e..599883c2 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,4 +1,4 @@ -import { Accessor, callAll, Field, isEqual, Plugin } from 'roqueform'; +import { ValueAccessor, callAll, Field, isEqual, Plugin } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** diff --git a/packages/roqueform/src/main/composePlugins.ts b/packages/roqueform/src/main/composePlugins.ts index fabdfa7a..e8d287e4 100644 --- a/packages/roqueform/src/main/composePlugins.ts +++ b/packages/roqueform/src/main/composePlugins.ts @@ -1,53 +1,57 @@ -import { Plugin } from './shared-types'; +import { PluginCallback } from './typings'; /** * Composes a plugin from multiple plugins. */ -export function composePlugins(a: Plugin, b: Plugin): Plugin; +export function composePlugins(a: PluginCallback, b: PluginCallback): PluginCallback; /** * Composes a plugin from multiple plugins. */ -export function composePlugins(a: Plugin, b: Plugin, c: Plugin): Plugin; +export function composePlugins( + a: PluginCallback, + b: PluginCallback, + c: PluginCallback +): PluginCallback; /** * Composes a plugin from multiple plugins. */ export function composePlugins( - a: Plugin, - b: Plugin, - c: Plugin, - d: Plugin -): Plugin; + a: PluginCallback, + b: PluginCallback, + c: PluginCallback, + d: PluginCallback +): PluginCallback; /** * Composes a plugin from multiple plugins. */ export function composePlugins( - a: Plugin, - b: Plugin, - c: Plugin, - d: Plugin, - e: Plugin -): Plugin; + a: PluginCallback, + b: PluginCallback, + c: PluginCallback, + d: PluginCallback, + e: PluginCallback +): PluginCallback; /** * Composes a plugin from multiple plugins. */ export function composePlugins( - a: Plugin, - b: Plugin, - c: Plugin, - d: Plugin, - e: Plugin, - f: Plugin, - ...other: Plugin[] -): Plugin; + a: PluginCallback, + b: PluginCallback, + c: PluginCallback, + d: PluginCallback, + e: PluginCallback, + f: PluginCallback, + ...other: PluginCallback[] +): PluginCallback; -export function composePlugins(...plugins: Plugin[]): Plugin { - return (field, accessor, notify) => { +export function composePlugins(...plugins: PluginCallback[]): PluginCallback { + return field => { for (const plugin of plugins) { - plugin(field, accessor, notify); + plugin(field); } return field; }; diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 5480ce39..157da441 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,5 +1,5 @@ -import { Accessor, Field, Plugin } from './shared-types'; -import { callAll, callOrGet, isEqual } from './utils'; +import { ValueAccessor, ValueChangeEvent, Field, PluginCallback, Event } from './typings'; +import { callOrGet, isEqual } from './utils'; import { naturalAccessor } from './naturalAccessor'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types @@ -19,166 +19,161 @@ export function createField(): Field; * @param accessor Resolves values for derived fields. * @template Value The root field value. */ -export function createField(initialValue: Value, accessor?: Accessor): Field; +export function createField(initialValue: Value, accessor?: ValueAccessor): Field; /** * Creates the new field instance. * * @param initialValue The initial value assigned to the field. * @param plugin The plugin that enhances the field. - * @param accessor Resolves values for derived fields. - * @template Value The root field value. - * @template Mixin The mixin added by the plugin. + * @param valueAccessor Resolves values for derived fields. + * @template Value The root field initial value. + * @template Plugin The plugin added to the field. */ -export function createField( +export function createField( initialValue: Value, - plugin: Plugin>, - accessor?: Accessor -): Field & Mixin; - -export function createField(initialValue?: unknown, plugin?: Plugin | Accessor, accessor?: Accessor) { + plugin: PluginCallback>, + valueAccessor?: ValueAccessor +): Field; + +export function createField( + initialValue?: unknown, + plugin?: PluginCallback | ValueAccessor, + valueAccessor?: ValueAccessor +) { if (typeof plugin !== 'function') { plugin = undefined; - accessor = plugin; + valueAccessor = plugin; } - return getOrCreateFieldController(accessor || naturalAccessor, null, null, initialValue, plugin)._field; + return getOrCreateField(valueAccessor || naturalAccessor, null, null, initialValue, plugin || null); } -interface FieldController { - _parent: FieldController | null; - - /** - * The map from a child key to a corresponding controller. - */ - _childrenMap: Map | null; - _children: FieldController[] | null; - _field: Field; - _key: unknown; - _value: unknown; - _isTransient: boolean; - _accessor: Accessor; - _notify: (updatedField: Field) => void; -} - -function getOrCreateFieldController( - accessor: Accessor, - parentController: FieldController | null, +function getOrCreateField( + valueAccessor: ValueAccessor, + parent: Field | null, key: unknown, initialValue: unknown, - plugin: Plugin | undefined -): FieldController { - let parent: Field | null = null; - - if (parentController !== null) { - const child = parentController._childrenMap?.get(key); + plugin: PluginCallback | null +): Field { + let child: Field; - if (child !== undefined) { - return child; - } - - parent = parentController._field; - initialValue = accessor.get(parentController._value, key); + if (parent !== null && parent.childrenMap !== null && (child = parent.childrenMap.get(key)!) !== undefined) { + return child; } - const listeners: Array<(updatedField: Field, currentField: Field) => void> = []; - - const notify = (updatedField: Field): void => { - callAll(listeners, [updatedField, controller._field]); - }; - - const field = { - setValue(value) { - applyValue(controller, callOrGet(value, [controller._value]), false); + child = { + key, + value: null, + initialValue, + isTransient: false, + root: null!, + parent, + children: [], + childrenMap: new Map(), + eventListeners: Object.create(null), + valueAccessor, + plugin, + setValue: value => { + setValue(child, callOrGet(value, child.value), false); }, - setTransientValue(value) { - applyValue(controller, callOrGet(value, [controller._value]), true); + setTransientValue: value => { + setValue(child, callOrGet(value, child.value), true); }, - dispatch() { - applyValue(controller, controller._value, false); + propagate: () => { + setValue(child, child.value, false); }, - at(key) { - return getOrCreateFieldController(controller._accessor, controller, key, null, plugin)._field; + at: key => { + return getOrCreateField(child.valueAccessor, child, key, null, plugin); }, - subscribe(listener) { - if (typeof listener === 'function' && listeners.indexOf(listener) === -1) { + on: (type, listener: (event: any) => void) => { + let listeners = child.eventListeners[type]; + + if (listeners !== undefined) { listeners.push(listener); + } else { + listeners = child.eventListeners[type] = [listener]; } return () => { listeners.splice(listeners.indexOf(listener), 1); }; }, - } as Field; - - Object.defineProperties(field, { - parent: { enumerable: true, value: parent }, - key: { enumerable: true, value: key }, - value: { enumerable: true, get: () => controller._value }, - isTransient: { enumerable: true, get: () => controller._isTransient }, - }); - - const controller: FieldController = { - _parent: parentController, - _childrenMap: null, - _children: null, - _field: field, - _key: key, - _value: initialValue, - _isTransient: false, - _notify: notify, - _accessor: accessor, }; - plugin?.(field, accessor, () => notify(controller._field)); + child.root = child; - if (parentController !== null) { - (parentController._childrenMap ||= new Map()).set(key, controller); - (parentController._children ||= []).push(controller); + if (parent !== null) { + child.root = parent.root; + child.value = valueAccessor.get(parent.value, key); + child.initialValue = valueAccessor.get(parent.initialValue, key); } - return controller; + plugin?.(child); + + if (parent !== null) { + parent.children.push(child); + parent.childrenMap.set(child.key, child); + } + + return child; +} + +function callAll(listeners: Array<(event: Event) => void> | undefined, event: Event): void { + if (listeners === undefined) { + return; + } + for (const listener of listeners) { + try { + listener(event); + } catch (error) { + setTimeout(() => { + throw error; + }, 0); + } + } +} + +function dispatchEvents(events: Event[]): void { + for (const event of events) { + callAll(event.currentTarget.eventListeners[event.type], event); + callAll(event.currentTarget.eventListeners['*'], event); + } } -function applyValue(controller: FieldController, value: unknown, transient: boolean): void { - if (isEqual(controller._value, value) && controller._isTransient === transient) { +function setValue(field: Field, value: unknown, transient: boolean): void { + if (isEqual(field.value, value) && field.isTransient === transient) { return; } - controller._isTransient = transient; + field.isTransient = transient; - let rootController = controller; + let changeRoot = field; - while (rootController._parent !== null && !rootController._isTransient) { - const { _key } = rootController; - rootController = rootController._parent; - value = controller._accessor.set(rootController._value, _key, value); + while (changeRoot.parent !== null && !changeRoot.isTransient) { + value = field.valueAccessor.set(changeRoot.parent.value, changeRoot.key, value); + changeRoot = changeRoot.parent; } - callAll(propagateValue(controller, rootController, value, []), [controller._field]); + dispatchEvents(applyValue(field, changeRoot, value, [])); } -function propagateValue( - targetController: FieldController, - controller: FieldController, - value: unknown, - notifyCallbacks: FieldController['_notify'][] -): FieldController['_notify'][] { - notifyCallbacks.push(controller._notify); +function applyValue(target: Field, field: Field, value: unknown, events: ValueChangeEvent[]): ValueChangeEvent[] { + events.push({ type: 'change', target, currentTarget: field, previousValue: field.value }); - controller._value = value; + field.value = value; - if (controller._children !== null) { - for (const child of controller._children) { - if (child._isTransient) { + if (field.children !== null) { + for (const child of field.children) { + if (child.isTransient) { continue; } - const childValue = controller._accessor.get(value, child._key); - if (child !== targetController && isEqual(child._value, childValue)) { + const childValue = field.valueAccessor.get(value, child.key); + if (child !== target && isEqual(child.value, childValue)) { continue; } - propagateValue(targetController, child, childValue, notifyCallbacks); + applyValue(target, child, childValue, events); } } - return notifyCallbacks; + return events; } diff --git a/packages/roqueform/src/main/naturalAccessor.ts b/packages/roqueform/src/main/naturalAccessor.ts index fd0bdf58..ae780d10 100644 --- a/packages/roqueform/src/main/naturalAccessor.ts +++ b/packages/roqueform/src/main/naturalAccessor.ts @@ -1,10 +1,10 @@ -import { Accessor } from './shared-types'; +import { ValueAccessor } from './typings'; import { isEqual } from './utils'; /** * The accessor that reads and writes key-value pairs to well-known object instances. */ -export const naturalAccessor: Accessor = { +export const naturalAccessor: ValueAccessor = { get(obj, key) { if (isPrimitive(obj)) { return undefined; diff --git a/packages/roqueform/src/main/shared-types.ts b/packages/roqueform/src/main/shared-types.ts deleted file mode 100644 index 5dd83e71..00000000 --- a/packages/roqueform/src/main/shared-types.ts +++ /dev/null @@ -1,147 +0,0 @@ -// prettier-ignore -type KeyOf = - T extends Function | Date | RegExp | string | number | boolean | bigint | symbol | undefined | null ? never : - T extends { set(key: any, value: any): any, get(key: infer K): any } ? K : - T extends { add(value: any): any, [Symbol.iterator]: Function } ? number : - T extends readonly any[] ? number : - T extends object ? keyof T : - never - -// prettier-ignore -type ValueAt = - T extends { set(key: any, value: any): any, get(key: infer K): infer V } ? Key extends K ? V : undefined : - T extends { add(value: infer V): any, [Symbol.iterator]: Function } ? Key extends number ? V | undefined : undefined : - T extends readonly any[] ? Key extends number ? T[Key] : undefined : - Key extends keyof T ? T[Key] : - undefined - -/** - * Makes all object properties mutable. - */ -type Mutable = { -readonly [P in keyof T]: T[P] }; - -/** - * The key in {@link Plugin} that stores the root field value type. - */ -declare const ROOT_VALUE: unique symbol; - -/** - * The callback that enhances the field. - * - * The plugin should _mutate_ the passed field instance. - * - * @template Mixin The mixin added by the plugin. - * @template Value The root field value. - */ -export interface Plugin { - /** - * @param field The field that must be enhanced. - * @param accessor The accessor that reads and writes object properties. - * @param notify Synchronously notifies listeners of the field. - */ - (field: Mutable, accessor: Accessor, notify: () => void): void; - - /** - * Prevents root field value type erasure. - * - * @internal - */ - [ROOT_VALUE]?: Value; -} - -/** - * The abstraction used by the {@link Field} to read and write object properties. - */ -export interface Accessor { - /** - * Returns the value that corresponds to `key` in `obj`. - * - * @param obj An arbitrary object from which the value must be read. May be `undefined` or `null`. - * @param key The key to read. - * @returns The value in `obj` that corresponds to the `key`. - */ - get(obj: any, key: any): any; - - /** - * Returns the object updated where the `key` is associated with `value`. - * - * @param obj The object to update. May be `undefined` or `null`. - * @param key The key to write. - * @param value The value to associate with the `key`. - * @returns The updated object. - */ - set(obj: any, key: any, value: any): any; -} - -/** - * The field that holds a value and provides means to update it. Fields can be enhanced by plugins that provide such - * things as integration with rendering and validation libraries. - * - * @template Value The root field value. - * @template Mixin The mixin added by the plugin. - */ -export interface Field { - /** - * The parent field from which this one was derived, or `null` if there's no parent. - */ - readonly parent: (Field & Mixin) | null; - - /** - * The key in the parent value that corresponds to the value controlled by the field, or `null` if there's no parent. - */ - readonly key: any; - - /** - * The current value of the field. - */ - readonly value: Value; - - /** - * `true` if the value was last updated using {@link setTransientValue}, or `false` otherwise. - */ - readonly isTransient: boolean; - - /** - * Updates the value of the field and notifies both ancestors and derived fields. If field withholds - * {@link isTransient a transient value} then it becomes non-transient. - * - * @param value The value to set or a callback that receives a previous value and returns a new one. - */ - setValue(value: Value | ((prevValue: Value) => Value)): void; - - /** - * Updates the value of the field and notifies only derived fields and marks value as {@link isTransient transient}. - * - * @param value The value to set or a callback that receives a previous value and returns a new one. - */ - setTransientValue(value: Value | ((prevValue: Value) => Value)): void; - - /** - * If the current value {@link isTransient is transient} then `dispatch` notifies the parent about this value and - * marks value as non-transient. No-op otherwise. - */ - dispatch(): void; - - /** - * Derives a new field that controls value that is stored under a key in the value of this field. - * - * @param key The key the derived field would control. - * @returns The derived {@link Field} instance. - * @template Key The key of the object value controlled by the field. - */ - at>(key: Key): Field, Mixin> & Mixin; - - /** - * Subscribes the listener to field updates. - * - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. - */ - subscribe( - /** - * @param updatedField The field that was updated. This can be ancestor, descendant, or the `currentField` itself. - * @param currentField The field to which the listener is subscribed. - */ - listener: (updatedField: Field & Mixin, currentField: Field & Mixin) => void - ): () => void; -} diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts new file mode 100644 index 00000000..c6d4513c --- /dev/null +++ b/packages/roqueform/src/main/typings.ts @@ -0,0 +1,243 @@ +/** + * The field describes field that holds a value and provides means to update it. Fields can be enhanced by plugins that + * provide such things as integration with rendering and validation libraries. + * + * @template Value The field value. + * @template Plugin The plugin added to the field. + */ +export type Field = FieldController & Plugin; + +/** + * The baseline of a field. + * + * @template Value The field value. + * @template Plugin The plugin added to the field. + */ +interface FieldController { + /** + * The key in the {@link parent parent value} that corresponds to the value of this field, or `null` if there's no + * parent. + */ + key: any; + + /** + * The current value of the field. + */ + value: Value; + + /** + * The initial value of the field. + */ + initialValue: Value; + + /** + * `true` if the value was last updated using {@link setTransientValue}, or `false` otherwise. + */ + isTransient: boolean; + + /** + * The root field. + */ + ['root']: Field; + + /** + * The parent field, or `null` if this is the root field. + */ + ['parent']: Field | null; + + /** + * The array of immediate child fields that were {@link at previously accessed}, or `null` if there's no children. + * + * @see {@link childrenMap} + */ + ['children']: Field[]; + + /** + * Mapping from a key to a child field. + * + * @see {@link children} + */ + ['childrenMap']: Map>; + + /** + * The map from an event type to an array of associated listeners, or `null` if no listeners were added. + */ + ['eventListeners']: { [eventType: string]: Array<(event: Event) => void> }; + + /** + * The accessor that reads the field value from the value of the parent fields, and updates parent value. + */ + ['valueAccessor']: ValueAccessor; + + /** + * The plugin that is applied to this field and all child field when they are accessed. + */ + ['plugin']: PluginCallback | null; + + /** + * Updates the field value and notifies both ancestor and child fields about the change. If the field withholds + * {@link isTransient a transient value} then it becomes non-transient. + * + * @param value The value to set, or a callback that receives a previous value and returns a new one. + */ + setValue(value: Value | ((prevValue: Value) => Value)): void; + + /** + * Updates the value of the field, notifies child fields about the change, and marks value as + * {@link isTransient transient}. + * + * @param value The value to set, or a callback that receives a previous value and returns a new one. + */ + setTransientValue(value: Value | ((prevValue: Value) => Value)): void; + + /** + * If {@link value the current value} {@link isTransient is transient} then the value of the parent field is notified + * about the change and this field is marked as non-transient. + */ + propagate(): void; + + /** + * Returns a child field that controls the value which is stored under the given key in + * {@link value the current value}. + * + * @param key The key in the value of this field. + * @returns The child field instance. + * @template Key The key in the value of this field. + */ + at>(key: Key): Field, Plugin>; + + /** + * Subscribes the listener to all events. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on(eventType: '*', listener: (event: Event) => void): () => void; + + /** + * Subscribes the listener to field value changes. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on(eventType: 'valueChanged', listener: (event: ValueChangeEvent) => void): () => void; +} + +/** + * The event dispatched to subscribers of {@link Field a field}. + * + * @template Value The field value. + * @template Plugin The plugin added to the field. + */ +export interface Event { + /** + * The type of the event. + */ + type: string; + + /** + * The field that caused the event to be dispatched. This can be ancestor, descendant, or the {@link currentTarget}. + */ + target: Field; + + /** + * The field to which the event listener is subscribed. + */ + currentTarget: Field; +} + +/** + * The event dispatched when the field value has changed. + * + * @template Value The field value. + * @template Plugin The plugin added to the field. + */ +export interface ValueChangeEvent extends Event { + type: 'change'; + + /** + * The previous value that was replaced by {@link Field.value the current field value}. + */ + previousValue: Value; +} + +/** + * The callback that enhances the field. + * + * The plugin should _mutate_ the passed field instance. + * + * @template Plugin The plugin added to the field. + * @template Value The root field value. + */ +export type PluginCallback = (field: Field) => void; + +/** + * Infers plugin from a field. + */ +export type InferPlugin = Field extends FieldController ? Plugin : unknown; + +/** + * The abstraction used by the {@link Field} to read and write object properties. + */ +export interface ValueAccessor { + /** + * Returns the value that corresponds to `key` in `obj`. + * + * @param obj An arbitrary object from which the value must be read. May be `undefined` or `null`. + * @param key The key to read. + * @returns The value in `obj` that corresponds to the `key`. + */ + get(obj: any, key: any): any; + + /** + * Returns the object updated where the `key` is associated with `value`. + * + * @param obj The object to update. May be `undefined` or `null`. + * @param key The key to write. + * @param value The value to associate with the `key`. + * @returns The updated object. + */ + set(obj: any, key: any, value: any): any; +} + +type Primitive = + | String + | Number + | Boolean + | BigInt + | Symbol + | Function + | Date + | RegExp + | string + | number + | boolean + | bigint + | symbol + | undefined + | null; + +/** + * The union of all keys of `T`, or `never` if keys cannot be extracted. + */ +// prettier-ignore +type KeyOf = + T extends Primitive ? never : + T extends { set(key: any, value: any): any, get(key: infer K): any } ? K : + T extends { add(value: any): any, [Symbol.iterator]: Function } ? number : + T extends readonly any[] ? number : + T extends object ? keyof T : + never + +/** + * The value that corresponds to a `Key` in an object `T`, or `never` if there's no such key. + */ +// prettier-ignore +type ValueAt = + T extends { set(key: any, value: any): any, get(key: infer K): infer V } ? Key extends K ? V : never : + T extends { add(value: infer V): any, [Symbol.iterator]: Function } ? Key extends number ? V | undefined : never : + T extends readonly any[] ? Key extends number ? T[Key] : never : + Key extends keyof T ? T[Key] : + never diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 0ac2e9e6..65cdefad 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,64 +1,5 @@ -/** - * If value is a function then it is called, otherwise the value is returned as is. - * - * @param value The value to return or a callback to call. - * @returns The value or the call result. - * @template T The returned value. - */ -export function callOrGet(value: T | (() => T)): T; - -/** - * If value is a function then it is called with the given set of arguments, otherwise the value is returned as is. - * - * @param value The value to return or a callback to call. - * @param args The array of arguments to pass to a callback. - * @returns The value or the call result. - * @template T The returned value. - * @template A The array of callback arguments. - */ -export function callOrGet(value: T | ((...args: A) => T), args: A): T; - -export function callOrGet(value: unknown, args?: unknown[]) { - return typeof value === 'function' ? value.apply(undefined, args) : value; -} - -/** - * Calls each callback from the array. - * - * If the array contains the same callback multiple times then the callback is called only once. If a callback throws an - * error, the remaining callbacks are still called and the error is re-thrown asynchronously. - * - * @param callbacks The array of callbacks. - */ -export function callAll(callbacks: Array<() => any>): void; - -/** - * Calls each callback from the array with given set of arguments. - * - * If the array contains the same callback multiple times then the callback is called only once. If a callback throws an - * error, the remaining callbacks are still called and the error is re-thrown asynchronously. - * - * @param callbacks The array of callbacks. - * @param args The array of arguments to pass to each callback. - * @template A The array of callback arguments. - */ -export function callAll(callbacks: Array<(...args: A) => any>, args: A): void; - -export function callAll(callbacks: Function[], args?: unknown[]): void { - for (let i = 0; i < callbacks.length; ++i) { - const cb = callbacks[i]; - - if (callbacks.lastIndexOf(cb) !== i) { - continue; - } - try { - cb.apply(undefined, args); - } catch (error) { - setTimeout(() => { - throw error; - }, 0); - } - } +export function callOrGet(value: T | ((prevValue: T) => T), prevValue: T): T { + return typeof value === 'function' ? (value as Function)(prevValue) : value; } /** diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index dc46efce..b2fa93d6 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,20 +1,21 @@ -import { Field, Plugin } from './shared-types'; -import { callAll, isEqual } from './utils'; +import { Field, PluginCallback } from './typings'; +import { isEqual } from './utils'; +import { InferPlugin } from './typings'; /** - * The mixin added to fields by the {@link validationPlugin}. + * The plugin that enables field value validation. * * @template Error The validation error. * @template Options Options passed to the validator. */ -export interface ValidationMixin { +export interface ValidationPlugin { /** * A validation error associated with the field, or `null` if there's no error. */ - readonly error: Error | null; + error: Error | null; /** - * `true` if the field or any of its derived fields have an associated error, or `false` otherwise. + * `true` if this field or any of its descendants have no associated errors, or `false` otherwise. */ readonly isInvalid: boolean; @@ -23,6 +24,35 @@ export interface ValidationMixin { */ readonly isValidating: boolean; + /** + * The total number of errors associated with this field and its child fields. + */ + ['errorCount']: number; + + /** + * The type of the associated error: + * - `external` if an error was set using {@link ValidationPlugin.setError}; + * - `validation` if an error was set using {@link ValidationPlugin.setValidationError}; + * - `null` if there's no associated error. + */ + ['errorType']: 'external' | 'validation' | null; + + /** + * The validator to which the field value validation is delegated. + */ + ['validator']: Validator; + + /** + * The field that initiated the validation, or `null` if there's no pending validation. + */ + ['validationRoot']: Field> | null; + + /** + * The abort controller that aborts the signal passed to {@link Validator.validateAsync}, or `null` if there's no + * pending async validation. + */ + ['validationAbortController']: AbortController | null; + /** * Associates an error with the field and notifies the subscribers. * @@ -40,16 +70,20 @@ export interface ValidationMixin { */ clearErrors(): void; + /** + * Returns all fields that have an error. + */ + getInvalidFields(): Field>[]; + /** * Triggers a sync field validation. Starting a validation will clear errors that were set during the previous * validation and preserve errors set via {@link setError}. If you want to clear all errors before the validation, * use {@link clearErrors}. * * @param options Options passed to the validator. - * @returns The array of validation errors returned by the {@link Validator.validate}, or `null` if there are no - * errors. + * @returns `true` if {@link isInvalid field is valid}, or `false` otherwise. */ - validate(options?: Options): Error[] | null; + validate(options?: Options): boolean; /** * Triggers an async field validation. Starting a validation will clear errors that were set during the previous @@ -57,48 +91,55 @@ export interface ValidationMixin { * use {@link clearErrors}. * * @param options Options passed to the validator. - * @returns The array of validation errors returned by the {@link Validator.validateAsync}, or `null` if there are no - * errors. + * @returns `true` if {@link isInvalid field is valid}, or `false` otherwise. */ - validateAsync(options?: Options): Promise; + validateAsync(options?: Options): Promise; /** * Aborts async validation of the field or no-op if there's no pending validation. If the field's parent is being * validated then parent validation proceeds but this field won't be updated with validation errors. */ abortValidation(): void; + + /** + * Associates an internal error with the field and notifies the subscribers. Use this method in + * {@link Validator validators} to set errors that would be overridden during the next validation. + * + * @param error The error to set. + */ + ['setValidationError'](error: Error): void; } /** - * The validator implements the library-specific validation logic. + * The validator implements the validation rules. * * @template Error The validation error. * @template Options Options passed to the validator. */ -export interface Validator { +export interface Validator { /** * The callback that applies validation rules to a field. * - * @param field The field where {@link ValidationMixin.validate} was called. - * @param setError The callback that associates an error with the field. - * @param options The options passed to the {@link ValidationMixin.validate} method. + * Set {@link ValidationPlugin.setValidationError validation errors} to invalid fields during validation. + * + * @param field The field where {@link ValidationPlugin.validate} was called. + * @param options The options passed to the {@link ValidationPlugin.validate} method. */ - validate(field: Field, setError: (field: Field, error: Error) => void, options: Options | undefined): void; + validate(field: Field>, options: Options | undefined): void; /** * The callback that applies validation rules to a field. * - * @param field The field where {@link ValidationMixin.validate} was called. - * @param setError The callback that associates an error with the field. - * @param options The options passed to the {@link ValidationMixin.validate} method. - * @param signal The signal that is aborted if the validation process should be stopped. + * Check that {@link ValidationPlugin.validationAbortController validation isn't aborted} before + * {@link ValidationPlugin.setValidationError setting a validation error}, otherwise stop validation as soon as + * possible. + * + * If this callback is omitted, then {@link validate} would be called instead. + * + * @param field The field where {@link ValidationPlugin.validateAsync} was called. + * @param options The options passed to the {@link ValidationPlugin.validateAsync} method. */ - validateAsync?( - field: Field, - setError: (field: Field, error: Error) => void, - options: Options | undefined, - signal: AbortSignal - ): Promise; + validateAsync?(field: Field>, options: Options | undefined): Promise; } /** @@ -116,65 +157,45 @@ export interface Validator { */ export function validationPlugin( validator: Validator | Validator['validate'] -): Plugin, Value> { - let controllerMap: WeakMap; - - return (field, _accessor, notify) => { - controllerMap ||= new WeakMap(); - - if (controllerMap.has(field)) { - return; - } - - const controller: FieldController = { - _parent: null, - _children: null, - _field: field, - _errorCount: 0, - _isErrored: false, - _error: null, - _isInternal: false, - _validator: typeof validator === 'function' ? { validate: validator } : validator, - _initiator: null, - _validationNonce: 0, - _abortController: null, - _controllerMap: controllerMap, - _notify: notify, - }; - - controllerMap.set(field, controller); - - if (field.parent !== null) { - const parent = controllerMap.get(field.parent)!; - - controller._parent = parent; - controller._initiator = parent._initiator; - - (parent._children ||= []).push(controller); - } - - const { setTransientValue, setValue } = field; +): PluginCallback, Value> { + return field => { + field.error = null; + field.errorCount = 0; + field.errorType = null; + field.validator = typeof validator === 'function' ? { validate: validator } : validator; + field.validationRoot = null; + field.validationAbortController = null; Object.defineProperties(field, { - error: { enumerable: true, get: () => controller._error }, - isInvalid: { enumerable: true, get: () => controller._errorCount !== 0 }, - isValidating: { enumerable: true, get: () => controller._initiator !== null }, + isInvalid: { get: () => field.errorCount !== 0 }, + isValidating: { get: () => field.validationRoot !== null }, }); - field.setTransientValue = value => { - if (controller._initiator !== null) { - callAll(endValidation(controller, controller._initiator, true, [])); - } - setTransientValue(value); - }; + const { setValue, setTransientValue } = field; + + // field.setError = null; + // field.deleteError = null; + // field.clearErrors = null; + // field.getInvalidFields = null; + // field.validate = null; + // field.validateAsync = null; + // field.abortValidation = null; + // field.setValidationError = null; field.setValue = value => { - if (controller._initiator !== null) { - callAll(endValidation(controller, controller._initiator, true, [])); + if (field.validationRoot !== null) { + callAll(endValidation(field, field.validationRoot, true, [])); } setValue(value); }; + field.setTransientValue = value => { + if (controller.validationRoot !== null) { + callAll(endValidation(controller, controller.validationRoot, true, [])); + } + setTransientValue(value); + }; + field.setError = error => { callAll(setError(controller, error, false, [])); }; @@ -357,7 +378,7 @@ function beginValidation( initiator: FieldController, notifyCallbacks: Array<() => void> ): Array<() => void> { - controller._initiator = initiator; + controller.validationRoot = initiator; if (initiator._abortController) { notifyCallbacks.push(controller._notify); @@ -388,11 +409,11 @@ function endValidation( aborted: boolean, notifyCallbacks: Array<() => void> ): Array<() => void> { - if (controller._initiator !== initiator) { + if (controller.validationRoot !== initiator) { return notifyCallbacks; } - controller._initiator = null; + controller.validationRoot = null; if (initiator._abortController) { notifyCallbacks.push(controller._notify); @@ -424,7 +445,7 @@ function endValidation( function validate(controller: FieldController, options: unknown): any[] | null { const notifyCallbacks: Array<() => void> = []; - if (controller._initiator === controller) { + if (controller.validationRoot === controller) { endValidation(controller, controller, true, notifyCallbacks); } @@ -439,7 +460,7 @@ function validate(controller: FieldController, options: unknown): any[] | null { const targetController = controller._controllerMap.get(targetField); if ( targetController !== undefined && - targetController._initiator === controller && + targetController.validationRoot === controller && controller._validationNonce === validationNonce ) { (errors ||= []).push(error); @@ -468,7 +489,7 @@ function validate(controller: FieldController, options: unknown): any[] | null { function validateAsync(controller: FieldController, options: unknown): Promise { const notifyCallbacks: Array<() => void> = []; - if (controller._initiator === controller) { + if (controller.validationRoot === controller) { endValidation(controller, controller, true, notifyCallbacks); } @@ -488,7 +509,7 @@ function validateAsync(controller: FieldController, options: unknown): Promise Date: Tue, 7 Nov 2023 23:34:19 +0300 Subject: [PATCH 02/33] Refactored validationPlugin --- README.md | 8 +- packages/react/src/main/AccessorContext.ts | 2 +- packages/reset-plugin/src/main/resetPlugin.ts | 2 +- packages/roqueform/src/main/createField.ts | 90 ++-- .../roqueform/src/main/naturalAccessor.ts | 4 +- packages/roqueform/src/main/typings.ts | 65 +-- packages/roqueform/src/main/utils.ts | 27 + .../roqueform/src/main/validationPlugin.ts | 468 ++++++++---------- packages/zod-plugin/src/main/zodPlugin.ts | 2 +- 9 files changed, 296 insertions(+), 372 deletions(-) diff --git a/README.md b/README.md index a03a9339..a3117a3f 100644 --- a/README.md +++ b/README.md @@ -282,20 +282,20 @@ planetsField.at(1).value; # Accessors -[`ValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates +[`Accessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates field values. - When the new field is derived via [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#at) method, the field value is read from the value of the parent field using the - [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. + [`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. - When a field value is updated via [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue), then the parent field value is updated with the value returned from the - [`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the + [`Accessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the updated field has derived fields, their values are updated with values returned from the - [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. + [`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. You can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses [`naturalAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalAccessor.html): diff --git a/packages/react/src/main/AccessorContext.ts b/packages/react/src/main/AccessorContext.ts index 2a271a40..0d2a0625 100644 --- a/packages/react/src/main/AccessorContext.ts +++ b/packages/react/src/main/AccessorContext.ts @@ -1,5 +1,5 @@ import { createContext, Context } from 'react'; -import { naturalAccessor, ValueAccessor } from 'roqueform'; +import { naturalAccessor, Accessor } from 'roqueform'; /** * The context that is used by {@link useField} to retrieve an accessor. diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 599883c2..416bb88e 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,4 +1,4 @@ -import { ValueAccessor, callAll, Field, isEqual, Plugin } from 'roqueform'; +import { Accessor, callAll, Field, isEqual, Plugin } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 157da441..1fcfc600 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,5 +1,5 @@ -import { ValueAccessor, ValueChangeEvent, Field, PluginCallback, Event } from './typings'; -import { callOrGet, isEqual } from './utils'; +import { Accessor, Field, PluginCallback, ValueChangeEvent } from './typings'; +import { callOrGet, dispatchEvents, isEqual } from './utils'; import { naturalAccessor } from './naturalAccessor'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types @@ -19,37 +19,33 @@ export function createField(): Field; * @param accessor Resolves values for derived fields. * @template Value The root field value. */ -export function createField(initialValue: Value, accessor?: ValueAccessor): Field; +export function createField(initialValue: Value, accessor?: Accessor): Field; /** * Creates the new field instance. * * @param initialValue The initial value assigned to the field. * @param plugin The plugin that enhances the field. - * @param valueAccessor Resolves values for derived fields. + * @param accessor Resolves values for derived fields. * @template Value The root field initial value. * @template Plugin The plugin added to the field. */ export function createField( initialValue: Value, plugin: PluginCallback>, - valueAccessor?: ValueAccessor -): Field; - -export function createField( - initialValue?: unknown, - plugin?: PluginCallback | ValueAccessor, - valueAccessor?: ValueAccessor -) { + accessor?: Accessor +): Field; + +export function createField(initialValue?: unknown, plugin?: PluginCallback | Accessor, accessor?: Accessor) { if (typeof plugin !== 'function') { plugin = undefined; - valueAccessor = plugin; + accessor = plugin; } - return getOrCreateField(valueAccessor || naturalAccessor, null, null, initialValue, plugin || null); + return getOrCreateField(accessor || naturalAccessor, null, null, initialValue, plugin || null); } function getOrCreateField( - valueAccessor: ValueAccessor, + accessor: Accessor, parent: Field | null, key: unknown, initialValue: unknown, @@ -62,16 +58,17 @@ function getOrCreateField( } child = { + __plugin: undefined, key, value: null, initialValue, isTransient: false, root: null!, parent, - children: [], - childrenMap: new Map(), - eventListeners: Object.create(null), - valueAccessor, + children: null, + childrenMap: null, + listeners: null, + accessor, plugin, setValue: value => { setValue(child, callOrGet(value, child.value), false); @@ -83,16 +80,11 @@ function getOrCreateField( setValue(child, child.value, false); }, at: key => { - return getOrCreateField(child.valueAccessor, child, key, null, plugin); + return getOrCreateField(child.accessor, child, key, null, plugin); }, - on: (type, listener: (event: any) => void) => { - let listeners = child.eventListeners[type]; - - if (listeners !== undefined) { - listeners.push(listener); - } else { - listeners = child.eventListeners[type] = [listener]; - } + on: (type, listener) => { + let listeners: unknown[]; + (listeners = (child.listeners ||= Object.create(null))[type] ||= []).push(listener); return () => { listeners.splice(listeners.indexOf(listener), 1); }; @@ -103,42 +95,20 @@ function getOrCreateField( if (parent !== null) { child.root = parent.root; - child.value = valueAccessor.get(parent.value, key); - child.initialValue = valueAccessor.get(parent.initialValue, key); + child.value = accessor.get(parent.value, key); + child.initialValue = accessor.get(parent.initialValue, key); } plugin?.(child); if (parent !== null) { - parent.children.push(child); - parent.childrenMap.set(child.key, child); + (parent.children ||= []).push(child); + (parent.childrenMap ||= new Map()).set(child.key, child); } return child; } -function callAll(listeners: Array<(event: Event) => void> | undefined, event: Event): void { - if (listeners === undefined) { - return; - } - for (const listener of listeners) { - try { - listener(event); - } catch (error) { - setTimeout(() => { - throw error; - }, 0); - } - } -} - -function dispatchEvents(events: Event[]): void { - for (const event of events) { - callAll(event.currentTarget.eventListeners[event.type], event); - callAll(event.currentTarget.eventListeners['*'], event); - } -} - function setValue(field: Field, value: unknown, transient: boolean): void { if (isEqual(field.value, value) && field.isTransient === transient) { return; @@ -149,15 +119,15 @@ function setValue(field: Field, value: unknown, transient: boolean): void { let changeRoot = field; while (changeRoot.parent !== null && !changeRoot.isTransient) { - value = field.valueAccessor.set(changeRoot.parent.value, changeRoot.key, value); + value = field.accessor.set(changeRoot.parent.value, changeRoot.key, value); changeRoot = changeRoot.parent; } - dispatchEvents(applyValue(field, changeRoot, value, [])); + dispatchEvents(propagateValue(field, changeRoot, value, [])); } -function applyValue(target: Field, field: Field, value: unknown, events: ValueChangeEvent[]): ValueChangeEvent[] { - events.push({ type: 'change', target, currentTarget: field, previousValue: field.value }); +function propagateValue(target: Field, field: Field, value: unknown, events: ValueChangeEvent[]): ValueChangeEvent[] { + events.push({ type: 'valueChange', target, currentTarget: field, previousValue: field.value }); field.value = value; @@ -167,11 +137,11 @@ function applyValue(target: Field, field: Field, value: unknown, events: ValueCh continue; } - const childValue = field.valueAccessor.get(value, child.key); + const childValue = field.accessor.get(value, child.key); if (child !== target && isEqual(child.value, childValue)) { continue; } - applyValue(target, child, childValue, events); + propagateValue(target, child, childValue, events); } } diff --git a/packages/roqueform/src/main/naturalAccessor.ts b/packages/roqueform/src/main/naturalAccessor.ts index ae780d10..33ed0e99 100644 --- a/packages/roqueform/src/main/naturalAccessor.ts +++ b/packages/roqueform/src/main/naturalAccessor.ts @@ -1,10 +1,10 @@ -import { ValueAccessor } from './typings'; +import { Accessor } from './typings'; import { isEqual } from './utils'; /** * The accessor that reads and writes key-value pairs to well-known object instances. */ -export const naturalAccessor: ValueAccessor = { +export const naturalAccessor: Accessor = { get(obj, key) { if (isPrimitive(obj)) { return undefined; diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index c6d4513c..b1525efc 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -2,18 +2,23 @@ * The field describes field that holds a value and provides means to update it. Fields can be enhanced by plugins that * provide such things as integration with rendering and validation libraries. * - * @template Value The field value. * @template Plugin The plugin added to the field. + * @template Value The field value. */ -export type Field = FieldController & Plugin; +export type Field = FieldController & Plugin; /** - * The baseline of a field. + * The baseline field controller that can be enhanced by plugins. * - * @template Value The field value. * @template Plugin The plugin added to the field. + * @template Value The field value. */ -interface FieldController { +interface FieldController { + /** + * @internal + */ + ['__plugin']: Plugin; + /** * The key in the {@link parent parent value} that corresponds to the value of this field, or `null` if there's no * parent. @@ -38,36 +43,37 @@ interface FieldController { /** * The root field. */ - ['root']: Field; + ['root']: Field; /** * The parent field, or `null` if this is the root field. */ - ['parent']: Field | null; + ['parent']: Field | null; /** * The array of immediate child fields that were {@link at previously accessed}, or `null` if there's no children. + * Children array is always in sync with {@link childrenMap}. * - * @see {@link childrenMap} + * Don't modify this array directly and always use on {@link at} to add a new child. */ - ['children']: Field[]; + ['children']: Field[] | null; /** - * Mapping from a key to a child field. + * Mapping from a key to a child field. Children map is always in sync with {@link children children array}. * - * @see {@link children} + * Don't modify this array directly and always use on {@link at} to add a new child. */ - ['childrenMap']: Map>; + ['childrenMap']: Map> | null; /** * The map from an event type to an array of associated listeners, or `null` if no listeners were added. */ - ['eventListeners']: { [eventType: string]: Array<(event: Event) => void> }; + ['listeners']: { [eventType: string]: Array<(event: Event) => void> } | null; /** * The accessor that reads the field value from the value of the parent fields, and updates parent value. */ - ['valueAccessor']: ValueAccessor; + ['accessor']: Accessor; /** * The plugin that is applied to this field and all child field when they are accessed. @@ -104,7 +110,7 @@ interface FieldController { * @returns The child field instance. * @template Key The key in the value of this field. */ - at>(key: Key): Field, Plugin>; + at>(key: Key): Field>; /** * Subscribes the listener to all events. @@ -113,25 +119,25 @@ interface FieldController { * @param listener The listener that would be triggered. * @returns The callback to unsubscribe the listener. */ - on(eventType: '*', listener: (event: Event) => void): () => void; + on(eventType: '*', listener: (event: Event) => void): () => void; /** - * Subscribes the listener to field value changes. + * Subscribes the listener to field value change events. * * @param eventType The type of the event. * @param listener The listener that would be triggered. * @returns The callback to unsubscribe the listener. */ - on(eventType: 'valueChanged', listener: (event: ValueChangeEvent) => void): () => void; + on(eventType: 'valueChange', listener: (event: ValueChangeEvent) => void): () => void; } /** * The event dispatched to subscribers of {@link Field a field}. * - * @template Value The field value. * @template Plugin The plugin added to the field. + * @template Value The field value. */ -export interface Event { +export interface Event { /** * The type of the event. */ @@ -140,22 +146,22 @@ export interface Event { /** * The field that caused the event to be dispatched. This can be ancestor, descendant, or the {@link currentTarget}. */ - target: Field; + target: Field; /** * The field to which the event listener is subscribed. */ - currentTarget: Field; + currentTarget: Field; } /** * The event dispatched when the field value has changed. * - * @template Value The field value. * @template Plugin The plugin added to the field. + * @template Value The field value. */ -export interface ValueChangeEvent extends Event { - type: 'change'; +export interface ValueChangeEvent extends Event { + type: 'valueChange'; /** * The previous value that was replaced by {@link Field.value the current field value}. @@ -171,17 +177,12 @@ export interface ValueChangeEvent extends Event = (field: Field) => void; - -/** - * Infers plugin from a field. - */ -export type InferPlugin = Field extends FieldController ? Plugin : unknown; +export type PluginCallback = (field: Field) => void; /** * The abstraction used by the {@link Field} to read and write object properties. */ -export interface ValueAccessor { +export interface Accessor { /** * Returns the value that corresponds to `key` in `obj`. * diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 65cdefad..ade0155d 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,3 +1,5 @@ +import { Event } from './typings'; + export function callOrGet(value: T | ((prevValue: T) => T), prevValue: T): T { return typeof value === 'function' ? (value as Function)(prevValue) : value; } @@ -12,3 +14,28 @@ export function callOrGet(value: T | ((prevValue: T) => T), prevValue: T): T export function isEqual(a: unknown, b: unknown): boolean { return a === b || (a !== a && b !== b); } + +export function dispatchEvents(events: readonly Event[]): void { + for (const event of events) { + const { listeners } = event.currentTarget; + + if (listeners !== null) { + callAll(listeners[event.type], event); + callAll(listeners['*'], event); + } + } +} + +function callAll(listeners: Array<(event: Event) => void> | undefined, event: Event): void { + if (listeners !== undefined) { + for (const listener of listeners) { + try { + listener(event); + } catch (error) { + setTimeout(() => { + throw error; + }, 0); + } + } + } +} diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index b2fa93d6..7bb77656 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,6 +1,10 @@ -import { Field, PluginCallback } from './typings'; -import { isEqual } from './utils'; -import { InferPlugin } from './typings'; +import { Event, Field, PluginCallback } from './typings'; +import { dispatchEvents, isEqual } from './utils'; + +const EVENT_VALIDITY_CHANGE = 'validityChange'; +const EVENT_VALIDATION_START = 'validationStart'; +const EVENT_VALIDATION_END = 'validationEnd'; +const ERROR_VALIDATION_ABORTED = 'Validation aborted'; /** * The plugin that enables field value validation. @@ -9,6 +13,16 @@ import { InferPlugin } from './typings'; * @template Options Options passed to the validator. */ export interface ValidationPlugin { + /** + * @internal + */ + ['__plugin']: unknown; + + /** + * @internal + */ + value: unknown; + /** * A validation error associated with the field, or `null` if there's no error. */ @@ -31,11 +45,11 @@ export interface ValidationPlugin { /** * The type of the associated error: - * - `external` if an error was set using {@link ValidationPlugin.setError}; - * - `validation` if an error was set using {@link ValidationPlugin.setValidationError}; - * - `null` if there's no associated error. + * - 0 if there's no associated error. + * - 1 if an error was set using {@link ValidationPlugin.setValidationError}; + * - 2 if an error was set using {@link ValidationPlugin.setError}; */ - ['errorType']: 'external' | 'validation' | null; + ['errorOrigin']: 0 | 1 | 2; /** * The validator to which the field value validation is delegated. @@ -43,22 +57,22 @@ export interface ValidationPlugin { ['validator']: Validator; /** - * The field that initiated the validation, or `null` if there's no pending validation. + * The field where the validation was triggered, or `null` if there's no pending validation. */ - ['validationRoot']: Field> | null; + ['validationRoot']: Field | null; /** - * The abort controller that aborts the signal passed to {@link Validator.validateAsync}, or `null` if there's no - * pending async validation. + * The abort controller associated with the pending {@link Validator.validateAsync async validation}, or `null` if + * there's no pending async validation. Abort controller is only defined for {@link validationRoot}. */ ['validationAbortController']: AbortController | null; /** * Associates an error with the field and notifies the subscribers. * - * @param error The error to set. + * @param error The error to set. If the passed error is `null` of `undefined` then an error is deleted. */ - setError(error: Error): void; + setError(error: Error | null | undefined): void; /** * Deletes an error associated with this field. @@ -73,7 +87,7 @@ export interface ValidationPlugin { /** * Returns all fields that have an error. */ - getInvalidFields(): Field>[]; + getInvalidFields(): Field[]; /** * Triggers a sync field validation. Starting a validation will clear errors that were set during the previous @@ -101,6 +115,37 @@ export interface ValidationPlugin { */ abortValidation(): void; + /** + * Subscribes the listener field validity change events. An {@link error} would contain the associated error. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on(eventType: 'validityChange', listener: (event: Event) => void): () => void; + + /** + * Subscribes the listener to validation start events. The event is triggered for all fields that are going to be + * validated. The {@link FieldController.value current value} of the field is the one that is being validated. + * {@link Event.target} points to the field where validation was triggered. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on(eventType: 'validationStart', listener: (event: Event) => void): () => void; + + /** + * Subscribes the listener to validation start end events. The event is triggered for all fields that were validated. + * {@link Event.target} points to the field where validation was triggered. Check {@link isInvalid} to detect the + * actual validity status. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on(eventType: 'validationEnd', listener: (event: Event) => void): () => void; + /** * Associates an internal error with the field and notifies the subscribers. Use this method in * {@link Validator validators} to set errors that would be overridden during the next validation. @@ -161,7 +206,7 @@ export function validationPlugin( return field => { field.error = null; field.errorCount = 0; - field.errorType = null; + field.errorOrigin = 0; field.validator = typeof validator === 'function' ? { validate: validator } : validator; field.validationRoot = null; field.validationAbortController = null; @@ -173,368 +218,249 @@ export function validationPlugin( const { setValue, setTransientValue } = field; - // field.setError = null; - // field.deleteError = null; - // field.clearErrors = null; - // field.getInvalidFields = null; - // field.validate = null; - // field.validateAsync = null; - // field.abortValidation = null; - // field.setValidationError = null; - field.setValue = value => { if (field.validationRoot !== null) { - callAll(endValidation(field, field.validationRoot, true, [])); + dispatchEvents(endValidation(field, field.validationRoot, true, [])); } setValue(value); }; field.setTransientValue = value => { - if (controller.validationRoot !== null) { - callAll(endValidation(controller, controller.validationRoot, true, [])); + if (field.validationRoot !== null) { + dispatchEvents(endValidation(field, field.validationRoot, true, [])); } setTransientValue(value); }; field.setError = error => { - callAll(setError(controller, error, false, [])); + dispatchEvents(setError(field, error, 2, [])); }; field.deleteError = () => { - callAll(deleteError(controller, false, [])); + dispatchEvents(deleteError(field, 2, [])); }; field.clearErrors = () => { - callAll(clearErrors(controller, false, [])); + dispatchEvents(clearErrors(field, 2, [])); }; - field.validate = options => validate(controller, options); + field.getInvalidFields = () => getInvalidFields(field, []); + + field.validate = options => validate(field, options); - field.validateAsync = options => validateAsync(controller, options); + field.validateAsync = options => validateAsync(field, options); field.abortValidation = () => { - callAll(endValidation(controller, controller, true, [])); + dispatchEvents(endValidation(field, field, true, [])); }; - }; -} - -interface FieldController { - _parent: FieldController | null; - _children: FieldController[] | null; - _field: Field; - - /** - * The total number of errors associated with the field and its derived fields. - */ - _errorCount: number; - - /** - * `true` if this field has an associated error, or `false` otherwise. - */ - _isErrored: boolean; - _error: unknown | null; - - /** - * `true` if an error was set internally by {@link ValidationMixin.validate}, or `false` if an issue was set by - * the user through {@link ValidationMixin.setError}. - */ - _isInternal: boolean; - _validator: Validator; - - /** - * The controller that initiated the subtree validation, or `null` if there's no pending validation. - */ - _initiator: FieldController | null; - - /** - * The number that is incremented every time a validation is started for {@link _field}. - */ - _validationNonce: number; - - /** - * The abort controller that aborts the signal passed to {@link Validator.validateAsync}. - */ - _abortController: AbortController | null; - - /** - * The controller map that maps all fields to a corresponding controller. - */ - _controllerMap: WeakMap; - /** - * Synchronously notifies listeners of the field. - */ - _notify: () => void; + field.setValidationError = error => { + dispatchEvents(setError(field, error, 1, [])); + }; + }; } -/** - * Associates a validation error with the field. - * - * @param controller The controller for which an error is set. - * @param error An error to set. - * @param internal Must be `true` if an error is set internally (during {@link ValidationMixin.validate} call), or - * `false` if an error is set using {@link ValidationMixin.setError} call. - * @param notifyCallbacks The in-out array of callbacks that notify affected fields. - * @returns An array of callbacks that notify affected fields. - */ function setError( - controller: FieldController, + field: Field, error: unknown, - internal: boolean, - notifyCallbacks: Array<() => void> -): Array<() => void> { - if (controller._isErrored && isEqual(controller._error, error) && controller._isInternal === internal) { - return notifyCallbacks; + errorOrigin: 1 | 2, + events: Event[] +): Event[] { + if (error === null || error === undefined) { + return deleteError(field, errorOrigin, events); } - controller._error = error; - controller._isInternal = internal; + const errored = field.error !== null; - notifyCallbacks.push(controller._notify); + if (errored && isEqual(field.error, error) && field.errorOrigin === errorOrigin) { + return events; + } + + field.error = error; + field.errorOrigin = errorOrigin; + + events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: field }); - if (controller._isErrored) { - return notifyCallbacks; + if (errored) { + return events; } - controller._errorCount++; - controller._isErrored = true; + field.errorCount++; - for (let ancestor = controller._parent; ancestor !== null; ancestor = ancestor._parent) { - if (ancestor._errorCount++ === 0) { - notifyCallbacks.push(ancestor._notify); + for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { + if (ancestor.errorCount++ === 0) { + events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: ancestor }); } } - return notifyCallbacks; + return events; } -/** - * Deletes a validation error from the field. - * - * @param controller The controller for which an error must be deleted. - * @param internal If `true` then only errors set by {@link ValidationMixin.validate} are deleted, otherwise all errors - * are deleted. - * @param notifyCallbacks The in-out array of callbacks that notify affected fields. - * @returns An array of callbacks that notify affected fields. - */ function deleteError( - controller: FieldController, - internal: boolean, - notifyCallbacks: Array<() => void> -): Array<() => void> { - if (!controller._isErrored || (internal && !controller._isInternal)) { - return notifyCallbacks; + field: Field, + errorOrigin: 1 | 2, + events: Event[] +): Event[] { + if (field.error === null || field.errorOrigin > errorOrigin) { + return events; } - controller._error = null; - controller._errorCount--; - controller._isInternal = controller._isErrored = false; + field.error = null; + field.errorOrigin = 0; + field.errorCount--; - notifyCallbacks.push(controller._notify); + events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: field }); - for (let ancestor = controller._parent; ancestor !== null; ancestor = ancestor._parent) { - if (--ancestor._errorCount === 0) { - notifyCallbacks.push(ancestor._notify); + for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { + if (--ancestor.errorCount === 0) { + events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: ancestor }); } } - return notifyCallbacks; + return events; } -/** - * Recursively deletes errors associated with the field and all of its derived fields. - * - * @param controller The controller tree root. - * @param internal If `true` then only errors set by {@link ValidationMixin.validate} are deleted, otherwise all errors - * are deleted. - * @param notifyCallbacks The in-out array of callbacks that notify affected fields. - * @returns An array of callbacks that notify affected fields. - */ function clearErrors( - controller: FieldController, - internal: boolean, - notifyCallbacks: Array<() => void> -): Array<() => void> { - deleteError(controller, internal, notifyCallbacks); - - if (controller._children !== null) { - for (const child of controller._children) { - clearErrors(child, internal, notifyCallbacks); + field: Field, + errorOrigin: 1 | 2, + events: Event[] +): Event[] { + deleteError(field, errorOrigin, events); + + if (field.children !== null) { + for (const child of field.children) { + clearErrors(child, errorOrigin, events); } } - return notifyCallbacks; + return events; } -/** - * Marks the controller as being validated by assigning an initiator to the controller and all of its children. - * - * @param controller The controller to which the initiator must be assigned. - * @param initiator The controller that initiated the validation process. - * @param notifyCallbacks The in-out array of callbacks that notify affected fields. - * @returns An array of callbacks that notify affected fields. - */ -function beginValidation( - controller: FieldController, - initiator: FieldController, - notifyCallbacks: Array<() => void> -): Array<() => void> { - controller.validationRoot = initiator; - - if (initiator._abortController) { - notifyCallbacks.push(controller._notify); - } +function startValidation( + field: Field, + validationRoot: Field, + events: Event[] +): Event[] { + field.validationRoot = validationRoot; + + events.push({ type: EVENT_VALIDATION_START, target: validationRoot, currentTarget: field }); - if (controller._children !== null) { - for (const child of controller._children) { - if (!child._field.isTransient) { - beginValidation(child, initiator, notifyCallbacks); + if (field.children !== null) { + for (const child of field.children) { + if (!child.isTransient) { + startValidation(child, validationRoot, events); } } } - return notifyCallbacks; + return events; } -/** - * Aborts the pending validation that was begun by the initiator. - * - * @param controller The controller that is being validated. - * @param initiator The controller that initiated validation process. - * @param aborted If `true` then the abort signal is aborted, otherwise it is ignored. - * @param notifyCallbacks The in-out array of callbacks that notify affected fields. - * @returns An array of callbacks that notify affected fields. - */ function endValidation( - controller: FieldController, - initiator: FieldController, + field: Field, + validationRoot: Field, aborted: boolean, - notifyCallbacks: Array<() => void> -): Array<() => void> { - if (controller.validationRoot !== initiator) { - return notifyCallbacks; + events: Event[] +): Event[] { + if (field.validationRoot !== validationRoot) { + return events; } - controller.validationRoot = null; + field.validationRoot = null; - if (initiator._abortController) { - notifyCallbacks.push(controller._notify); - } + events.push({ type: EVENT_VALIDATION_END, target: validationRoot, currentTarget: field }); - if (controller._children !== null) { - for (const child of controller._children) { - endValidation(child, initiator, aborted, notifyCallbacks); + if (field.children !== null) { + for (const child of field.children) { + endValidation(child, validationRoot, aborted, events); } } - if (controller._abortController !== null) { + if (field.validationAbortController !== null) { if (aborted) { - controller._abortController.abort(); + field.validationAbortController.abort(); } - controller._abortController = null; + field.validationAbortController = null; } - return notifyCallbacks; + return events; } -/** - * Synchronously validates the field and its derived fields and notifies them on change. - * - * @param controller The controller that must be validated. - * @param options Options passed to the validator. - * @returns The array of validation errors, or `null` if there are no errors. - */ -function validate(controller: FieldController, options: unknown): any[] | null { - const notifyCallbacks: Array<() => void> = []; +function validate(field: Field, options: unknown): boolean { + const events: Event[] = []; - if (controller.validationRoot === controller) { - endValidation(controller, controller, true, notifyCallbacks); + if (field.validationRoot !== null) { + endValidation(field.validationRoot, field.validationRoot, true, events); } - clearErrors(controller, true, notifyCallbacks); - beginValidation(controller, controller, notifyCallbacks); - - const validationNonce = ++controller._validationNonce; - - let errors: unknown[] | null = null; + clearErrors(field, 1, events); + startValidation(field, field, events); + dispatchEvents(events); - const setErrorCallback = (targetField: Field, error: unknown): void => { - const targetController = controller._controllerMap.get(targetField); - if ( - targetController !== undefined && - targetController.validationRoot === controller && - controller._validationNonce === validationNonce - ) { - (errors ||= []).push(error); - setError(targetController, error, true, notifyCallbacks); - } - }; + if (field.validationRoot !== field) { + throw new Error(ERROR_VALIDATION_ABORTED); + } try { - controller._validator.validate(controller._field, setErrorCallback, options); + field.validator.validate(field, options); } catch (error) { - callAll(endValidation(controller, controller, false, notifyCallbacks)); + dispatchEvents(endValidation(field, field, false, [])); throw error; } - callAll(endValidation(controller, controller, false, notifyCallbacks)); - return errors; + dispatchEvents(endValidation(field, field, false, [])); + return field.errorCount === 0; } -/** - * Asynchronously validates the field and its derived fields and notifies them on change. - * - * @param controller The controller that must be validated. - * @param options Options passed to the validator. - * @returns The array of validation errors, or `null` if there are no errors. - */ -function validateAsync(controller: FieldController, options: unknown): Promise { - const notifyCallbacks: Array<() => void> = []; +function validateAsync(field: Field, options: unknown): Promise { + const events: Event[] = []; - if (controller.validationRoot === controller) { - endValidation(controller, controller, true, notifyCallbacks); + if (field.validationRoot !== null) { + endValidation(field.validationRoot, field.validationRoot, true, events); } - controller._abortController = new AbortController(); + field.validationAbortController = new AbortController(); - clearErrors(controller, true, notifyCallbacks); - beginValidation(controller, controller, notifyCallbacks); + clearErrors(field, 1, events); + startValidation(field, field, events); + dispatchEvents(events); - const abortSignal = controller._abortController.signal; - const validationNonce = ++controller._validationNonce; - - callAll(notifyCallbacks); - - let errors: unknown[] | null = null; - - const setErrorCallback = (targetField: Field, error: unknown): void => { - const targetController = controller._controllerMap.get(targetField); - if ( - targetController !== undefined && - targetController.validationRoot === controller && - controller._validationNonce === validationNonce - ) { - (errors ||= []).push(error); - callAll(setError(targetController, error, true, [])); - } - }; + if (field.validationRoot !== field) { + return Promise.reject(new Error(ERROR_VALIDATION_ABORTED)); + } - const { validate, validateAsync = validate } = controller._validator; + const { validate, validateAsync = validate } = field.validator; return Promise.race([ new Promise(resolve => { - // noinspection JSVoidFunctionReturnValueUsed - resolve(validateAsync(controller._field, setErrorCallback, options, abortSignal)); + resolve(validateAsync(field, options)); }), new Promise((_resolve, reject) => { - abortSignal.addEventListener('abort', () => reject(new Error('Validation aborted'))); + field.validationAbortController!.signal.addEventListener('abort', () => { + reject(new Error(ERROR_VALIDATION_ABORTED)); + }); }), ]).then( () => { - callAll(endValidation(controller, controller, false, [])); - return errors; + dispatchEvents(endValidation(field, field, false, [])); + return field.errorCount === 0; }, error => { - callAll(endValidation(controller, controller, false, [])); + dispatchEvents(endValidation(field, field, false, [])); throw error; } ); } + +function getInvalidFields( + field: Field, + invalidFields: Field[] +): Field[] { + if (field.error !== null) { + invalidFields.push(field.error); + } + if (field.children !== null) { + for (const child of field.children) { + getInvalidFields(child, invalidFields); + } + } + return invalidFields; +} diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index acb5afae..21b1af60 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -1,5 +1,5 @@ import { ParseParams, ZodErrorMap, ZodIssue, ZodIssueCode, ZodType, ZodTypeAny } from 'zod'; -import { ValueAccessor, Field, Plugin, ValidationMixin, validationPlugin } from 'roqueform'; +import { Accessor, Field, Plugin, ValidationMixin, validationPlugin } from 'roqueform'; /** * The mixin added to fields by the {@link zodPlugin}. From 21f1779e3a4a75a2801f486519b988862785157c Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 00:00:31 +0300 Subject: [PATCH 03/33] Refactored doubterPlugin --- README.md | 4 +- .../src/main/constraintValidationPlugin.ts | 6 +- .../doubter-plugin/src/main/doubterPlugin.ts | 108 +++++++++--------- .../src/test/doubterPlugin.test-d.ts | 4 +- packages/react/src/main/useField.ts | 8 +- packages/ref-plugin/src/main/refPlugin.ts | 6 +- packages/reset-plugin/src/main/resetPlugin.ts | 6 +- packages/roqueform/src/main/index.ts | 2 +- .../roqueform/src/main/validationPlugin.ts | 20 ++-- .../src/test/validationPlugin.test-d.ts | 4 +- .../src/main/scrollToErrorPlugin.ts | 8 +- packages/uncontrolled-plugin/README.md | 2 +- .../src/main/uncontrolledPlugin.ts | 6 +- packages/zod-plugin/src/main/zodPlugin.ts | 8 +- .../zod-plugin/src/test/zodPlugin.test-d.ts | 4 +- 15 files changed, 101 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index a3117a3f..2b4d57c3 100644 --- a/README.md +++ b/README.md @@ -389,14 +389,14 @@ has changed. Let's update the plugin implementation to trigger subscribers. ```ts import { Plugin } from 'roqueform'; -interface ElementMixin { +interface ElementPlugin { readonly element: Element | null; setElement(element: Element | null): void; } -const elementPlugin: Plugin = (field, accessor, notify) => { +const elementPlugin: Plugin = (field, accessor, notify) => { field.element = null; field.setElement = element => { diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 3781ba40..bbfd75d8 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -1,9 +1,9 @@ import { Field, Plugin } from 'roqueform'; /** - * The mixin added to fields by the {@link constraintValidationPlugin}. + * The plugin added to fields by the {@link constraintValidationPlugin}. */ -export interface ConstraintValidationMixin { +export interface ConstraintValidationPlugin { /** * An error associated with the field, or `null` if there's no error. */ @@ -67,7 +67,7 @@ export interface ConstraintValidationMixin { /** * Enhances fields with Constraint Validation API methods. */ -export function constraintValidationPlugin(): Plugin { +export function constraintValidationPlugin(): Plugin { let controllerMap: WeakMap; return (field, _accessor, notify) => { diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index b21f9db1..7047dac0 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -1,13 +1,21 @@ -import { AnyShape, Issue, ParseOptions, Shape } from 'doubter'; -import { Field, Plugin, ValidationMixin, validationPlugin } from 'roqueform'; - -const anyShape = new Shape(); +import { Issue, ParseOptions, Shape } from 'doubter'; +import { Field, PluginCallback, ValidationPlugin, validationPlugin } from 'roqueform'; /** - * The mixin added to fields by the {@link doubterPlugin}. + * The plugin added to fields by the {@link doubterPlugin}. */ -export interface DoubterMixin extends ValidationMixin { - setError(error: Issue | string): void; +export interface DoubterPlugin extends ValidationPlugin { + /** + * @internal + */ + value: unknown; + + /** + * The shape that Doubter uses to validate {@link FieldController.value the field value}. + */ + ['shape']: Shape | null; + + setError(error: Issue | string | null | undefined): void; } /** @@ -16,15 +24,45 @@ export interface DoubterMixin extends ValidationMixin { * @param shape The shape that parses the field value. * @template Value The root field value. */ -export function doubterPlugin(shape: Shape): Plugin { - let plugin: Plugin; +export function doubterPlugin(shape: Shape): PluginCallback { + let plugin; + + return field => { + plugin ||= validationPlugin({ + validate(field, options) { + const shape = (field as unknown as DoubterPlugin).shape; + if (shape === null) { + return; + } + const result = shape.try(field.value, Object.assign({ verbose: true }, options)); + if (!result.ok) { + setIssues(field, result.issues); + } + }, + + validateAsync(field, signal, options) { + const shape = (field as unknown as DoubterPlugin).shape; + if (shape === null) { + return Promise.resolve(); + } + return shape.tryAsync(field.value, Object.assign({ verbose: true }, options)).then(result => { + if (!result.ok && !signal.aborted) { + setIssues(field, result.issues); + } + }); + }, + }); - return (field, accessor, notify) => { - (plugin ||= createValidationPlugin(shape))(field, accessor, notify); + plugin(field); + + field.shape = field.parent !== null ? field.parent.shape?.at(field.key) || null : shape; const { setError } = field; field.setError = error => { + if (error === null || error === undefined) { + return setError(error); + } if (typeof error === 'string') { error = { message: error }; } @@ -36,54 +74,16 @@ export function doubterPlugin(shape: Shape): Plugin(); - - return validationPlugin({ - validate(field, setError, options) { - options = Object.assign({ verbose: true }, options); - - const result = getShape(field, shapeCache, rootShape).try(field.value, options); - - if (!result.ok) { - setIssues(field, result.issues, setError); - } - }, - - validateAsync(field, setError, options) { - options = Object.assign({ verbose: true }, options); - - return getShape(field, shapeCache, rootShape) - .tryAsync(field.value, options) - .then(result => { - if (!result.ok) { - setIssues(field, result.issues, setError); - } - }); - }, - }); -} - -function getShape(field: Field, shapeCache: WeakMap, rootShape: AnyShape): AnyShape { - let shape = shapeCache.get(field); - - if (shape === undefined) { - shape = field.parent === null ? rootShape : getShape(field.parent, shapeCache, rootShape).at(field.key) || anyShape; - shapeCache.set(field, shape); - } - return shape; -} - -function prependPath(field: Field, path: unknown[] | undefined): unknown[] | undefined { +function prependPath(field: Field, path: unknown[] | undefined): unknown[] | undefined { for (let ancestor = field; ancestor.parent !== null; ancestor = ancestor.parent) { (path ||= []).unshift(ancestor.key); } return path; } -function setIssues(field: Field, issues: Issue[], setError: (field: Field, error: Issue) => void): void { +function setIssues(validationRoot: Field, issues: Issue[]): void { for (const issue of issues) { - let targetField = field; + let targetField = validationRoot; if (Array.isArray(issue.path)) { for (const key of issue.path) { @@ -91,7 +91,7 @@ function setIssues(field: Field, issues: Issue[], setError: (field: Field, error } } - issue.path = prependPath(field, issue.path); - setError(targetField, issue); + issue.path = prependPath(validationRoot, issue.path); + targetField.setValidationError(issue); } } diff --git a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts index 5129e978..a3bb66ba 100644 --- a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts +++ b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts @@ -1,10 +1,10 @@ import * as d from 'doubter'; import { expectType } from 'tsd'; import { createField, Field } from 'roqueform'; -import { DoubterMixin, doubterPlugin } from '@roqueform/doubter-plugin'; +import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin'; const shape = d.object({ foo: d.object({ bar: d.string() }) }); -expectType & DoubterMixin>( +expectType & DoubterPlugin>( createField({ foo: { bar: 'aaa' } }, doubterPlugin(shape)) ); diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index 6bb807b8..a4fa9172 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -29,12 +29,12 @@ export function useField(initialValue: Value | (() => Value)): Field( +export function useField( initialValue: Value | (() => Value), - plugin: Plugin> -): Field & Mixin; + plugin: Plugin> +): Field & Plugin; export function useField(initialValue?: unknown, plugin?: Plugin) { const accessor = useContext(AccessorContext); diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index 4c262d77..c1c679d1 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -1,9 +1,9 @@ import { Plugin } from 'roqueform'; /** - * The mixin added to fields by the {@link refPlugin}. + * The plugin added to fields by the {@link refPlugin}. */ -export interface RefMixin { +export interface RefPlugin { /** * The DOM element associated with the field. */ @@ -46,7 +46,7 @@ export interface RefMixin { /** * Enables field-element association and simplifies focus control. */ -export function refPlugin(): Plugin { +export function refPlugin(): Plugin { return field => { const { refCallback } = field; diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 416bb88e..494de361 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -2,9 +2,9 @@ import { Accessor, callAll, Field, isEqual, Plugin } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** - * The mixin added to fields by the {@link resetPlugin}. + * The plugin added to fields by the {@link resetPlugin}. */ -export interface ResetMixin { +export interface ResetPlugin { /** * @internal */ @@ -41,7 +41,7 @@ export interface ResetMixin { */ export function resetPlugin( equalityChecker: (initialValue: any, value: any) => boolean = isDeepEqual -): Plugin { +): Plugin { let controllerMap: WeakMap; return (field, accessor, notify) => { diff --git a/packages/roqueform/src/main/index.ts b/packages/roqueform/src/main/index.ts index 12507b83..2beac6db 100644 --- a/packages/roqueform/src/main/index.ts +++ b/packages/roqueform/src/main/index.ts @@ -5,6 +5,6 @@ export * from './composePlugins'; export * from './createField'; export * from './naturalAccessor'; -export * from './shared-types'; +export * from './typings'; export * from './utils'; export * from './validationPlugin'; diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 7bb77656..07017378 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -59,7 +59,7 @@ export interface ValidationPlugin { /** * The field where the validation was triggered, or `null` if there's no pending validation. */ - ['validationRoot']: Field | null; + ['validationRoot']: Field | null; /** * The abort controller associated with the pending {@link Validator.validateAsync async validation}, or `null` if @@ -87,7 +87,7 @@ export interface ValidationPlugin { /** * Returns all fields that have an error. */ - getInvalidFields(): Field[]; + getInvalidFields(): Field[]; /** * Triggers a sync field validation. Starting a validation will clear errors that were set during the previous @@ -170,7 +170,7 @@ export interface Validator { * @param field The field where {@link ValidationPlugin.validate} was called. * @param options The options passed to the {@link ValidationPlugin.validate} method. */ - validate(field: Field>, options: Options | undefined): void; + validate(field: Field>, options: Options | undefined): void; /** * The callback that applies validation rules to a field. @@ -182,9 +182,14 @@ export interface Validator { * If this callback is omitted, then {@link validate} would be called instead. * * @param field The field where {@link ValidationPlugin.validateAsync} was called. + * @param signal The signal that indicates that validation was aborted. * @param options The options passed to the {@link ValidationPlugin.validateAsync} method. */ - validateAsync?(field: Field>, options: Options | undefined): Promise; + validateAsync?( + field: Field>, + signal: AbortSignal, + options: Options | undefined + ): Promise; } /** @@ -261,7 +266,7 @@ export function validationPlugin( } function setError( - field: Field, + field: Field, error: unknown, errorOrigin: 1 | 2, events: Event[] @@ -428,13 +433,14 @@ function validateAsync(field: Field, options: unknown): Promis } const { validate, validateAsync = validate } = field.validator; + const signal = field.validationAbortController!.signal; return Promise.race([ new Promise(resolve => { - resolve(validateAsync(field, options)); + resolve(validateAsync(field, signal, options)); }), new Promise((_resolve, reject) => { - field.validationAbortController!.signal.addEventListener('abort', () => { + signal.addEventListener('abort', () => { reject(new Error(ERROR_VALIDATION_ABORTED)); }); }), diff --git a/packages/roqueform/src/test/validationPlugin.test-d.ts b/packages/roqueform/src/test/validationPlugin.test-d.ts index 8f1c5392..10e714b0 100644 --- a/packages/roqueform/src/test/validationPlugin.test-d.ts +++ b/packages/roqueform/src/test/validationPlugin.test-d.ts @@ -1,4 +1,4 @@ import { expectType } from 'tsd'; -import { Plugin, ValidationMixin, validationPlugin } from 'roqueform'; +import { Plugin, ValidationPlugin, validationPlugin } from 'roqueform'; -expectType>>(validationPlugin(() => undefined)); +expectType>>(validationPlugin(() => undefined)); diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 5e0b2bb3..8ee7aaa6 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -8,9 +8,9 @@ export interface ScrollToErrorOptions extends ScrollIntoViewOptions { } /** - * The mixin added to fields by the {@link scrollToErrorPlugin}. + * The plugin added to fields by the {@link scrollToErrorPlugin}. */ -export interface ScrollToErrorMixin { +export interface ScrollToErrorPlugin { /** * @internal */ @@ -58,7 +58,7 @@ export interface ScrollToErrorMixin { * Use this plugin in conjunction with another plugin that adds validation methods and manages `error` property of each * field. */ -export function scrollToErrorPlugin(): Plugin { +export function scrollToErrorPlugin(): Plugin { let controllerMap: WeakMap; return field => { @@ -109,7 +109,7 @@ interface FieldController { * The array of controllers that can be scrolled to. */ _targetControllers: FieldController[]; - _field: Field & ScrollToErrorMixin; + _field: Field & ScrollToErrorPlugin; _element: Element | null; } diff --git a/packages/uncontrolled-plugin/README.md b/packages/uncontrolled-plugin/README.md index 388f9fba..5596ea19 100644 --- a/packages/uncontrolled-plugin/README.md +++ b/packages/uncontrolled-plugin/README.md @@ -59,7 +59,7 @@ export const App = () => { # Value coercion To associate field with a form element, pass -[`Field.refCallback`](https://smikhalevski.github.io/roqueform/interfaces/_roqueform_ref_plugin.RefMixin.html#refCallback) +[`Field.refCallback`](https://smikhalevski.github.io/roqueform/interfaces/_roqueform_ref_plugin.RefPlugin.html#refCallback) as a `ref` attribute of an `input`, `textarea`, or any other form element: ```tsx diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index afd2f313..36655c04 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -8,9 +8,9 @@ import { createElementValueAccessor, ElementValueAccessor } from './createElemen const elementValueAccessor = createElementValueAccessor(); /** - * The mixin added to fields by the {@link uncontrolledPlugin}. + * The plugin added to fields by the {@link uncontrolledPlugin}. */ -export interface UncontrolledMixin { +export interface UncontrolledPlugin { /** * The callback that associates the field with the DOM element. */ @@ -29,7 +29,7 @@ export interface UncontrolledMixin { * * @param accessor The accessor that reads and writes value to and from the DOM elements managed by the filed. */ -export function uncontrolledPlugin(accessor = elementValueAccessor): Plugin { +export function uncontrolledPlugin(accessor = elementValueAccessor): Plugin { return field => { const { refCallback } = field; diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index 21b1af60..8cb940d0 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -1,10 +1,10 @@ import { ParseParams, ZodErrorMap, ZodIssue, ZodIssueCode, ZodType, ZodTypeAny } from 'zod'; -import { Accessor, Field, Plugin, ValidationMixin, validationPlugin } from 'roqueform'; +import { Accessor, Field, Plugin, ValidationPlugin, validationPlugin } from 'roqueform'; /** - * The mixin added to fields by the {@link zodPlugin}. + * The plugin added to fields by the {@link zodPlugin}. */ -export interface ZodMixin extends ValidationMixin> { +export interface ZodPlugin extends ValidationPlugin> { setError(error: ZodIssue | string): void; } @@ -16,7 +16,7 @@ export interface ZodMixin extends ValidationMixin * @template Value The root field value. * @returns The validation plugin. */ -export function zodPlugin(type: ZodType, errorMap?: ZodErrorMap): Plugin { +export function zodPlugin(type: ZodType, errorMap?: ZodErrorMap): Plugin { let plugin: Plugin; return (field, accessor, notify) => { diff --git a/packages/zod-plugin/src/test/zodPlugin.test-d.ts b/packages/zod-plugin/src/test/zodPlugin.test-d.ts index 3fc297cb..4ab0429e 100644 --- a/packages/zod-plugin/src/test/zodPlugin.test-d.ts +++ b/packages/zod-plugin/src/test/zodPlugin.test-d.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; import { expectType } from 'tsd'; import { createField, Field } from 'roqueform'; -import { ZodMixin, zodPlugin } from '@roqueform/zod-plugin'; +import { ZodPlugin, zodPlugin } from '@roqueform/zod-plugin'; const shape = z.object({ foo: z.object({ bar: z.string() }) }); -expectType & ZodMixin>( +expectType & ZodPlugin>( createField({ foo: { bar: 'aaa' } }, zodPlugin(shape)) ); From 26fe0a207252205d050175be895ccc6e3c59c54f Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 14:45:46 +0300 Subject: [PATCH 04/33] Refactored resetPlugin --- .../doubter-plugin/src/main/doubterPlugin.ts | 95 ++++---- packages/reset-plugin/src/main/resetPlugin.ts | 165 ++++++------- packages/roqueform/src/main/createField.ts | 5 +- packages/roqueform/src/main/typings.ts | 10 +- .../roqueform/src/main/validationPlugin.ts | 230 ++++++++++-------- 5 files changed, 249 insertions(+), 256 deletions(-) diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index 7047dac0..a04360ef 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -1,19 +1,15 @@ -import { Issue, ParseOptions, Shape } from 'doubter'; -import { Field, PluginCallback, ValidationPlugin, validationPlugin } from 'roqueform'; +import { Err, Issue, Ok, ParseOptions, Shape } from 'doubter'; +import { Field, PluginCallback, Validation, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; /** * The plugin added to fields by the {@link doubterPlugin}. */ export interface DoubterPlugin extends ValidationPlugin { /** - * @internal + * The shape that Doubter uses to validate {@link FieldController.value the field value}, or `null` if there's no + * shape for this field. */ - value: unknown; - - /** - * The shape that Doubter uses to validate {@link FieldController.value the field value}. - */ - ['shape']: Shape | null; + ['shape']: Shape; setError(error: Issue | string | null | undefined): void; } @@ -28,70 +24,61 @@ export function doubterPlugin(shape: Shape): PluginCallback { - plugin ||= validationPlugin({ - validate(field, options) { - const shape = (field as unknown as DoubterPlugin).shape; - if (shape === null) { - return; - } - const result = shape.try(field.value, Object.assign({ verbose: true }, options)); - if (!result.ok) { - setIssues(field, result.issues); - } - }, - - validateAsync(field, signal, options) { - const shape = (field as unknown as DoubterPlugin).shape; - if (shape === null) { - return Promise.resolve(); - } - return shape.tryAsync(field.value, Object.assign({ verbose: true }, options)).then(result => { - if (!result.ok && !signal.aborted) { - setIssues(field, result.issues); - } - }); - }, - }); - - plugin(field); + (plugin ||= validationPlugin(doubterValidator))(field); - field.shape = field.parent !== null ? field.parent.shape?.at(field.key) || null : shape; + field.shape = field.parent === null ? shape : field.parent.shape?.at(field.key) || new Shape(); const { setError } = field; field.setError = error => { - if (error === null || error === undefined) { - return setError(error); - } - if (typeof error === 'string') { - error = { message: error }; + if (error !== null && error !== undefined) { + if (typeof error === 'string') { + error = { message: error }; + } + setPath(field, error); + error.input = field.value; } - error.path = prependPath(field, error.path); - error.input = field.value; - setError(error); }; }; } -function prependPath(field: Field, path: unknown[] | undefined): unknown[] | undefined { +const doubterValidator: Validator = { + validate(field, options) { + const { validation, shape } = field as unknown as Field; + + endValidation(validation!, shape.try(field.value, Object.assign({ verbose: true }, options))); + }, + + validateAsync(field, options) { + const { validation, shape } = field as unknown as Field; + + return shape.tryAsync(field.value, Object.assign({ verbose: true }, options)).then(result => { + endValidation(validation!, result); + }); + }, +}; + +function setPath(field: Field, issue: Issue): void { for (let ancestor = field; ancestor.parent !== null; ancestor = ancestor.parent) { - (path ||= []).unshift(ancestor.key); + (issue.path ||= []).unshift(ancestor.key); } - return path; } -function setIssues(validationRoot: Field, issues: Issue[]): void { - for (const issue of issues) { - let targetField = validationRoot; +function endValidation(validation: Validation, result: Err | Ok): void { + if (result.ok) { + return; + } + for (const issue of result.issues) { + let field = validation.root; - if (Array.isArray(issue.path)) { + if (issue.path !== undefined) { for (const key of issue.path) { - targetField = targetField.at(key); + field = field.at(key); } } - issue.path = prependPath(validationRoot, issue.path); - targetField.setValidationError(issue); + setPath(validation.root, issue); + field.setValidationError(validation, issue); } } diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 494de361..77b7161f 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,4 +1,4 @@ -import { Accessor, callAll, Field, isEqual, Plugin } from 'roqueform'; +import { dispatchEvents, Event, Field, isEqual, PluginCallback } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** @@ -8,17 +8,25 @@ export interface ResetPlugin { /** * @internal */ - readonly value: unknown; + ['__plugin']: unknown; + + /** + * @internal + */ + value: unknown; /** * `true` if the field value is different from its initial value, or `false` otherwise. */ - readonly isDirty: boolean; + isDirty: boolean; /** - * The initial field value. + * The callback that compares initial value and the current value of the field. + * + * @param initialValue The initial value. + * @param value The current value. */ - readonly initialValue: this['value']; + ['equalityChecker']: (initialValue: any, value: any) => boolean; /** * Sets the initial value of the field and notifies ancestors and descendants. @@ -31,6 +39,33 @@ export interface ResetPlugin { * Reverts the field to its initial value. */ reset(): void; + + /** + * Subscribes the listener to field initial value change events. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on( + eventType: 'initialValueChange', + listener: (event: InitialValueChangeEvent) => void + ): () => void; +} + +/** + * The event dispatched when the field initial value has changed. + * + * @template Plugin The plugin added to the field. + * @template Value The field value. + */ +export interface InitialValueChangeEvent extends Event { + type: 'initialValueChange'; + + /** + * The previous initial value that was replaced by {@link Field.initialValue the new initial value}. + */ + previousInitialValue: Value; } /** @@ -41,114 +76,64 @@ export interface ResetPlugin { */ export function resetPlugin( equalityChecker: (initialValue: any, value: any) => boolean = isDeepEqual -): Plugin { - let controllerMap: WeakMap; - - return (field, accessor, notify) => { - controllerMap ||= new WeakMap(); - - if (controllerMap.has(field)) { - return; - } - - const controller: FieldController = { - _parent: null, - _children: null, - _field: field, - _key: field.key, - _isDirty: false, - _initialValue: field.value, - _accessor: accessor, - _equalityChecker: equalityChecker, - _notify: notify, - }; - - controllerMap.set(field, controller); - - if (field.parent !== null) { - const parent = controllerMap.get(field.parent)!; - - controller._parent = parent; - controller._initialValue = accessor.get(parent._initialValue, controller._key); - - (parent._children ||= []).push(controller); - } - - Object.defineProperties(field, { - isDirty: { enumerable: true, get: () => controller._isDirty }, - initialValue: { enumerable: true, get: () => controller._initialValue }, - }); +): PluginCallback { + return field => { + field.isDirty = field.equalityChecker(field.initialValue, field.value); + field.equalityChecker = equalityChecker; field.setInitialValue = value => { - applyInitialValue(controller, value); + setInitialValue(field, value); }; field.reset = () => { - controller._field.setValue(controller._initialValue); + field.setValue(field.initialValue); }; - field.subscribe(() => { - applyDirty(controller); + field.on('valueChange', () => { + field.isDirty = field.equalityChecker(field.initialValue, field.value); }); - - applyDirty(controller); }; } -interface FieldController { - _parent: FieldController | null; - _children: FieldController[] | null; - _field: Field; - _key: unknown; - _isDirty: boolean; - _initialValue: unknown; - _accessor: Accessor; - _equalityChecker: (initialValue: any, value: any) => boolean; - _notify: () => void; -} - -function applyDirty(controller: FieldController): void { - controller._isDirty = !controller._equalityChecker(controller._initialValue, controller._field.value); -} - -function applyInitialValue(controller: FieldController, initialValue: unknown): void { - if (isEqual(controller._initialValue, initialValue)) { +function setInitialValue(field: Field, initialValue: unknown): void { + if (isEqual(field.initialValue, initialValue)) { return; } - let rootController = controller; + let root = field; - while (rootController._parent !== null) { - const { _key } = rootController; - rootController = rootController._parent; - initialValue = controller._accessor.set(rootController._initialValue, _key, initialValue); + while (root.parent !== null) { + initialValue = field.accessor.set(root.parent.value, root.key, initialValue); + root = root.parent; } - callAll(propagateInitialValue(controller, rootController, initialValue, [])); + dispatchEvents(propagateInitialValue(field, root, initialValue, [])); } function propagateInitialValue( - targetController: FieldController, - controller: FieldController, + target: Field, + field: Field, initialValue: unknown, - notifyCallbacks: Array<() => void> -): Array<() => void> { - notifyCallbacks.push(controller._notify); - - controller._initialValue = initialValue; - - applyDirty(controller); - - if (controller._children !== null) { - for (const child of controller._children) { - const childInitialValue = controller._accessor.get(initialValue, child._key); - - if (child !== targetController && isEqual(child._initialValue, childInitialValue)) { + events: InitialValueChangeEvent[] +): InitialValueChangeEvent[] { + events.push({ + type: 'initialValueChange', + target: target as Field, + currentTarget: field as Field, + previousInitialValue: field.initialValue, + }); + + field.initialValue = initialValue; + field.isDirty = field.equalityChecker(initialValue, field.initialValue); + + if (field.children !== null) { + for (const child of field.children) { + const childInitialValue = field.accessor.get(initialValue, child.key); + if (child !== target && isEqual(child.initialValue, childInitialValue)) { continue; } - propagateInitialValue(targetController, child, childInitialValue, notifyCallbacks); + propagateInitialValue(target, child, childInitialValue, events); } } - - return notifyCallbacks; + return events; } diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 1fcfc600..8d9f6d3a 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -10,7 +10,7 @@ type NoInfer = T extends infer T ? T : never; * * @template Value The root field value. */ -export function createField(): Field; +export function createField(): Field; /** * Creates the new field instance. @@ -19,7 +19,7 @@ export function createField(): Field; * @param accessor Resolves values for derived fields. * @template Value The root field value. */ -export function createField(initialValue: Value, accessor?: Accessor): Field; +export function createField(initialValue: Value, accessor?: Accessor): Field; /** * Creates the new field instance. @@ -144,6 +144,5 @@ function propagateValue(target: Field, field: Field, value: unknown, events: Val propagateValue(target, child, childValue, events); } } - return events; } diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index b1525efc..2b87614e 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -1,6 +1,6 @@ /** * The field describes field that holds a value and provides means to update it. Fields can be enhanced by plugins that - * provide such things as integration with rendering and validation libraries. + * provide integration with rendering frameworks, validation libraries, and other tools. * * @template Plugin The plugin added to the field. * @template Value The field value. @@ -23,7 +23,7 @@ interface FieldController { * The key in the {@link parent parent value} that corresponds to the value of this field, or `null` if there's no * parent. */ - key: any; + readonly key: any; /** * The current value of the field. @@ -48,7 +48,7 @@ interface FieldController { /** * The parent field, or `null` if this is the root field. */ - ['parent']: Field | null; + readonly ['parent']: Field | null; /** * The array of immediate child fields that were {@link at previously accessed}, or `null` if there's no children. @@ -73,12 +73,12 @@ interface FieldController { /** * The accessor that reads the field value from the value of the parent fields, and updates parent value. */ - ['accessor']: Accessor; + readonly ['accessor']: Accessor; /** * The plugin that is applied to this field and all child field when they are accessed. */ - ['plugin']: PluginCallback | null; + readonly ['plugin']: PluginCallback | null; /** * Updates the field value and notifies both ancestor and child fields about the change. If the field withholds diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 07017378..bcaa74ee 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,10 +1,26 @@ import { Event, Field, PluginCallback } from './typings'; import { dispatchEvents, isEqual } from './utils'; -const EVENT_VALIDITY_CHANGE = 'validityChange'; -const EVENT_VALIDATION_START = 'validationStart'; -const EVENT_VALIDATION_END = 'validationEnd'; -const ERROR_VALIDATION_ABORTED = 'Validation aborted'; +const EVENT_CHANGE = 'validityChange'; +const EVENT_START = 'validationStart'; +const EVENT_END = 'validationEnd'; +const ERROR_ABORT = 'Validation aborted'; + +/** + * Describes the pending validation. + */ +export interface Validation { + /** + * The field where the validation was triggered. + */ + readonly root: Field; + + /** + * The abort controller associated with the pending {@link Validator.validateAsync async validation}, or `null` if + * the validation is synchronous. + */ + readonly abortController: AbortController | null; +} /** * The plugin that enables field value validation. @@ -44,7 +60,7 @@ export interface ValidationPlugin { ['errorCount']: number; /** - * The type of the associated error: + * The origin of the associated error: * - 0 if there's no associated error. * - 1 if an error was set using {@link ValidationPlugin.setValidationError}; * - 2 if an error was set using {@link ValidationPlugin.setError}; @@ -57,20 +73,14 @@ export interface ValidationPlugin { ['validator']: Validator; /** - * The field where the validation was triggered, or `null` if there's no pending validation. + * The pending validation, or `null` if there's no pending validation. */ - ['validationRoot']: Field | null; - - /** - * The abort controller associated with the pending {@link Validator.validateAsync async validation}, or `null` if - * there's no pending async validation. Abort controller is only defined for {@link validationRoot}. - */ - ['validationAbortController']: AbortController | null; + ['validation']: Validation | null; /** * Associates an error with the field and notifies the subscribers. * - * @param error The error to set. If the passed error is `null` of `undefined` then an error is deleted. + * @param error The error to set. If `null` or `undefined` then an error is deleted. */ setError(error: Error | null | undefined): void; @@ -84,6 +94,11 @@ export interface ValidationPlugin { */ clearErrors(): void; + /** + * Returns all errors associated with this field and its child fields. + */ + getErrors(): Error[]; + /** * Returns all fields that have an error. */ @@ -147,12 +162,13 @@ export interface ValidationPlugin { on(eventType: 'validationEnd', listener: (event: Event) => void): () => void; /** - * Associates an internal error with the field and notifies the subscribers. Use this method in - * {@link Validator validators} to set errors that would be overridden during the next validation. + * Associates a validation error with the field and notifies the subscribers. Use this method in + * {@link Validator validators} to set errors that can be overridden during the next validation. * + * @param validation The validation in scope of which the value is set. * @param error The error to set. */ - ['setValidationError'](error: Error): void; + ['setValidationError'](validation: Validation, error: Error): void; } /** @@ -175,21 +191,16 @@ export interface Validator { /** * The callback that applies validation rules to a field. * - * Check that {@link ValidationPlugin.validationAbortController validation isn't aborted} before + * Check that {@link Validation.abortController validation isn't aborted} before * {@link ValidationPlugin.setValidationError setting a validation error}, otherwise stop validation as soon as * possible. * * If this callback is omitted, then {@link validate} would be called instead. * * @param field The field where {@link ValidationPlugin.validateAsync} was called. - * @param signal The signal that indicates that validation was aborted. * @param options The options passed to the {@link ValidationPlugin.validateAsync} method. */ - validateAsync?( - field: Field>, - signal: AbortSignal, - options: Options | undefined - ): Promise; + validateAsync?(field: Field>, options: Options | undefined): Promise; } /** @@ -205,34 +216,35 @@ export interface Validator { * @template Options Options passed to the validator. * @template Value The root field value. */ -export function validationPlugin( +export function validationPlugin( validator: Validator | Validator['validate'] -): PluginCallback, Value> { +): PluginCallback> { return field => { field.error = null; field.errorCount = 0; field.errorOrigin = 0; field.validator = typeof validator === 'function' ? { validate: validator } : validator; - field.validationRoot = null; - field.validationAbortController = null; + field.validation = null; Object.defineProperties(field, { isInvalid: { get: () => field.errorCount !== 0 }, - isValidating: { get: () => field.validationRoot !== null }, + isValidating: { get: () => field.validation !== null }, }); const { setValue, setTransientValue } = field; field.setValue = value => { - if (field.validationRoot !== null) { - dispatchEvents(endValidation(field, field.validationRoot, true, [])); + if (field.validation !== null) { + dispatchEvents(abortValidation(field, [])); } setValue(value); }; field.setTransientValue = value => { - if (field.validationRoot !== null) { - dispatchEvents(endValidation(field, field.validationRoot, true, [])); + if (field.validation !== null) { + dispatchEvents( + field.validation.root === field ? abortValidation(field, []) : endValidation(field, field.validation, []) + ); } setTransientValue(value); }; @@ -249,6 +261,8 @@ export function validationPlugin( dispatchEvents(clearErrors(field, 2, [])); }; + field.getErrors = () => convertToErrors(getInvalidFields(field, [])); + field.getInvalidFields = () => getInvalidFields(field, []); field.validate = options => validate(field, options); @@ -256,21 +270,25 @@ export function validationPlugin( field.validateAsync = options => validateAsync(field, options); field.abortValidation = () => { - dispatchEvents(endValidation(field, field, true, [])); + dispatchEvents(abortValidation(field, [])); }; - field.setValidationError = error => { - dispatchEvents(setError(field, error, 1, [])); + field.setValidationError = (validation, error) => { + if (validation !== null && field.validation !== validation && field.errorOrigin < 2) { + dispatchEvents(setError(field, error, 1, [])); + } }; }; } +type ValidationEvent = Event; + function setError( field: Field, error: unknown, errorOrigin: 1 | 2, - events: Event[] -): Event[] { + events: ValidationEvent[] +): ValidationEvent[] { if (error === null || error === undefined) { return deleteError(field, errorOrigin, events); } @@ -284,7 +302,7 @@ function setError( field.error = error; field.errorOrigin = errorOrigin; - events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: field }); + events.push({ type: EVENT_CHANGE, target: field, currentTarget: field }); if (errored) { return events; @@ -294,18 +312,14 @@ function setError( for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { if (ancestor.errorCount++ === 0) { - events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: ancestor }); + events.push({ type: EVENT_CHANGE, target: field, currentTarget: ancestor }); } } return events; } -function deleteError( - field: Field, - errorOrigin: 1 | 2, - events: Event[] -): Event[] { +function deleteError(field: Field, errorOrigin: 1 | 2, events: ValidationEvent[]): ValidationEvent[] { if (field.error === null || field.errorOrigin > errorOrigin) { return events; } @@ -314,26 +328,25 @@ function deleteError( field.errorOrigin = 0; field.errorCount--; - events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: field }); + events.push({ type: EVENT_CHANGE, target: field, currentTarget: field }); for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { if (--ancestor.errorCount === 0) { - events.push({ type: EVENT_VALIDITY_CHANGE, target: field, currentTarget: ancestor }); + events.push({ type: EVENT_CHANGE, target: field, currentTarget: ancestor }); } } return events; } -function clearErrors( - field: Field, - errorOrigin: 1 | 2, - events: Event[] -): Event[] { +function clearErrors(field: Field, errorOrigin: 1 | 2, events: ValidationEvent[]): ValidationEvent[] { deleteError(field, errorOrigin, events); if (field.children !== null) { for (const child of field.children) { + if (errorOrigin === 1 && child.isTransient) { + continue; + } clearErrors(child, errorOrigin, events); } } @@ -342,17 +355,17 @@ function clearErrors( function startValidation( field: Field, - validationRoot: Field, - events: Event[] -): Event[] { - field.validationRoot = validationRoot; + validation: Validation, + events: ValidationEvent[] +): ValidationEvent[] { + field.validation = validation; - events.push({ type: EVENT_VALIDATION_START, target: validationRoot, currentTarget: field }); + events.push({ type: EVENT_START, target: validation.root, currentTarget: field }); if (field.children !== null) { for (const child of field.children) { if (!child.isTransient) { - startValidation(child, validationRoot, events); + startValidation(child, validation, events); } } } @@ -361,112 +374,121 @@ function startValidation( function endValidation( field: Field, - validationRoot: Field, - aborted: boolean, - events: Event[] -): Event[] { - if (field.validationRoot !== validationRoot) { + validation: Validation, + events: ValidationEvent[] +): ValidationEvent[] { + if (field.validation !== validation) { return events; } - field.validationRoot = null; + field.validation = null; - events.push({ type: EVENT_VALIDATION_END, target: validationRoot, currentTarget: field }); + events.push({ type: EVENT_END, target: validation.root, currentTarget: field }); if (field.children !== null) { for (const child of field.children) { - endValidation(child, validationRoot, aborted, events); + endValidation(child, validation, events); } } - if (field.validationAbortController !== null) { - if (aborted) { - field.validationAbortController.abort(); - } - field.validationAbortController = null; - } + return events; +} +function abortValidation(field: Field, events: ValidationEvent[]): ValidationEvent[] { + const { validation } = field; + + if (validation !== null) { + endValidation(validation.root, validation, events); + validation.abortController?.abort(); + } return events; } function validate(field: Field, options: unknown): boolean { - const events: Event[] = []; + dispatchEvents(clearErrors(field, 1, abortValidation(field, []))); - if (field.validationRoot !== null) { - endValidation(field.validationRoot, field.validationRoot, true, events); + if (field.validation !== null) { + throw new Error(ERROR_ABORT); } - clearErrors(field, 1, events); - startValidation(field, field, events); - dispatchEvents(events); + const validation: Validation = { root: field, abortController: null }; + + dispatchEvents(startValidation(field, validation, [])); - if (field.validationRoot !== field) { - throw new Error(ERROR_VALIDATION_ABORTED); + if (field.validation !== validation) { + throw new Error(ERROR_ABORT); } try { field.validator.validate(field, options); } catch (error) { - dispatchEvents(endValidation(field, field, false, [])); + dispatchEvents(endValidation(field, validation, [])); throw error; } - dispatchEvents(endValidation(field, field, false, [])); + if (field.validation !== validation) { + throw new Error(ERROR_ABORT); + } + dispatchEvents(endValidation(field, validation, [])); return field.errorCount === 0; } function validateAsync(field: Field, options: unknown): Promise { - const events: Event[] = []; + dispatchEvents(clearErrors(field, 1, abortValidation(field, []))); - if (field.validationRoot !== null) { - endValidation(field.validationRoot, field.validationRoot, true, events); + if (field.validation !== null) { + return Promise.reject(new Error(ERROR_ABORT)); } - field.validationAbortController = new AbortController(); + const validation: Validation = { root: field, abortController: new AbortController() }; - clearErrors(field, 1, events); - startValidation(field, field, events); - dispatchEvents(events); + dispatchEvents(startValidation(field, validation, [])); - if (field.validationRoot !== field) { - return Promise.reject(new Error(ERROR_VALIDATION_ABORTED)); + if ((field.validation as Validation | null) !== validation) { + return Promise.reject(new Error(ERROR_ABORT)); } const { validate, validateAsync = validate } = field.validator; - const signal = field.validationAbortController!.signal; return Promise.race([ new Promise(resolve => { - resolve(validateAsync(field, signal, options)); + resolve(validateAsync(field, options)); }), new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => { - reject(new Error(ERROR_VALIDATION_ABORTED)); + validation.abortController!.signal.addEventListener('abort', () => { + reject(new Error(ERROR_ABORT)); }); }), ]).then( () => { - dispatchEvents(endValidation(field, field, false, [])); + if (field.validation !== validation) { + throw new Error(ERROR_ABORT); + } + dispatchEvents(endValidation(field, validation, [])); return field.errorCount === 0; }, error => { - dispatchEvents(endValidation(field, field, false, [])); + dispatchEvents(endValidation(field, validation, [])); throw error; } ); } -function getInvalidFields( - field: Field, - invalidFields: Field[] -): Field[] { +function getInvalidFields(field: Field, batch: Field[]): Field[] { if (field.error !== null) { - invalidFields.push(field.error); + batch.push(field.error); } if (field.children !== null) { for (const child of field.children) { - getInvalidFields(child, invalidFields); + getInvalidFields(child, batch); } } - return invalidFields; + return batch; +} + +function convertToErrors(batch: Field[]): any[] { + for (let i = 0; i < batch.length; ++i) { + batch[i] = batch[i].error; + } + return batch; } From 9996f1a60ecaf0f1f14f17337d249878ecd261e8 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 14:58:23 +0300 Subject: [PATCH 05/33] Refactored refPlugin --- packages/ref-plugin/src/main/refPlugin.ts | 24 +++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index c1c679d1..3ed621c0 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from 'roqueform'; +import { PluginCallback } from 'roqueform'; /** * The plugin added to fields by the {@link refPlugin}. @@ -7,7 +7,7 @@ export interface RefPlugin { /** * The DOM element associated with the field. */ - readonly element: Element | null; + element: Element | null; /** * The callback that associates the field with the {@link element DOM element}. @@ -46,32 +46,30 @@ export interface RefPlugin { /** * Enables field-element association and simplifies focus control. */ -export function refPlugin(): Plugin { +export function refPlugin(): PluginCallback { return field => { - const { refCallback } = field; - - let targetElement: Element | null = null; + field.element = null; - Object.defineProperty(field, 'element', { enumerable: true, get: () => targetElement }); + const { refCallback } = field; field.refCallback = element => { - targetElement = element instanceof Element ? element : null; + field.element = element instanceof Element ? element : null; refCallback?.(element); }; field.scrollIntoView = options => { - targetElement?.scrollIntoView(options); + field.element?.scrollIntoView(options); }; field.focus = options => { - if (isFocusable(targetElement)) { - targetElement.focus(options); + if (isFocusable(field.element)) { + field.element.focus(options); } }; field.blur = () => { - if (isFocusable(targetElement)) { - targetElement.blur(); + if (isFocusable(field.element)) { + field.element.blur(); } }; }; From 387fcc370ed9232f13c02aff2f2c80ece936f5bf Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 15:26:38 +0300 Subject: [PATCH 06/33] Refactored scrollToErrorPlugin --- .../src/main/scrollToErrorPlugin.ts | 93 +++++++------------ 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 8ee7aaa6..7057ecf1 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -1,4 +1,4 @@ -import { Field, Plugin } from 'roqueform'; +import { Field, PluginCallback } from 'roqueform'; export interface ScrollToErrorOptions extends ScrollIntoViewOptions { /** @@ -14,7 +14,12 @@ export interface ScrollToErrorPlugin { /** * @internal */ - readonly error: unknown; + error: unknown; + + /** + * The DOM element associated with the field. + */ + element: Element | null; /** * The callback that associates the field with the DOM element. @@ -58,79 +63,53 @@ export interface ScrollToErrorPlugin { * Use this plugin in conjunction with another plugin that adds validation methods and manages `error` property of each * field. */ -export function scrollToErrorPlugin(): Plugin { - let controllerMap: WeakMap; - +export function scrollToErrorPlugin(): PluginCallback { return field => { - controllerMap ||= new WeakMap(); - - if (controllerMap.has(field)) { - return; - } - - const controller: FieldController = { - _parent: field.parent !== null ? controllerMap.get(field.parent)! : null, - _targetControllers: [], - _field: field, - _element: null, - }; - - controllerMap.set(field, controller); - - for (let ancestor: FieldController | null = controller; ancestor !== null; ancestor = ancestor._parent) { - ancestor._targetControllers!.push(controller); - } - const { refCallback } = field; field.refCallback = element => { - controller._element = element; + field.element = element; refCallback?.(element); }; field.scrollToError = (index = 0, options) => { const rtl = options === null || typeof options !== 'object' || options.direction !== 'ltr'; - const controllers = controller._targetControllers.filter(hasVisibleError); - const targetController = sortByBoundingRect(controllers, rtl)[index < 0 ? controllers.length + index : index]; + const targets = getTargetFields(field, []); - if (targetController === undefined) { + if (targets.length === 0) { return false; } - targetController._element!.scrollIntoView(options); + + const target = sortByBoundingRect(targets, rtl)[index < 0 ? targets.length + index : index]; + + if (target !== undefined) { + target.element!.scrollIntoView(options); + } return true; }; }; } -interface FieldController { - _parent: FieldController | null; +function getTargetFields( + field: Field, + batch: Field[] +): Field[] { + if (field.error !== null && field.element !== null) { + const rect = field.element.getBoundingClientRect(); - /** - * The array of controllers that can be scrolled to. - */ - _targetControllers: FieldController[]; - _field: Field & ScrollToErrorPlugin; - _element: Element | null; -} - -function hasVisibleError(controller: FieldController): boolean { - if (controller._element === null || controller._field.error === null) { - return false; + if (rect.top !== 0 || rect.left !== 0 || rect.width !== 0 || rect.height !== 0) { + batch.push(field); + } } - - const rect = controller._element.getBoundingClientRect(); - - // Exclude non-displayed elements - return rect.top !== 0 || rect.left !== 0 || rect.width !== 0 || rect.height !== 0; + if (field.children !== null) { + for (const child of field.children) { + getTargetFields(child, batch); + } + } + return batch; } -/** - * Sorts controllers by their visual position. - * - * @param controllers The controllers to sort. All controllers must have a ref with an element. - * @param rtl The sorting order for elements. - */ -function sortByBoundingRect(controllers: FieldController[], rtl: boolean): FieldController[] { +function sortByBoundingRect(fields: Field[], rtl: boolean): Field[] { const { body, documentElement } = document; const scrollY = window.pageYOffset || documentElement.scrollTop || body.scrollTop; @@ -139,9 +118,9 @@ function sortByBoundingRect(controllers: FieldController[], rtl: boolean): Field const scrollX = window.pageXOffset || documentElement.scrollLeft || body.scrollLeft; const clientX = documentElement.clientLeft || body.clientLeft || 0; - return controllers.sort((controller1, controller2) => { - const rect1 = controller1._element!.getBoundingClientRect(); - const rect2 = controller2._element!.getBoundingClientRect(); + return fields.sort((field1, field2) => { + const rect1 = field1.element!.getBoundingClientRect(); + const rect2 = field2.element!.getBoundingClientRect(); const y1 = Math.round(rect1.top + scrollY - clientY); const y2 = Math.round(rect2.top + scrollY - clientY); From 71fb387a08769f78e47722d6b4527afc46ae0248 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 15:35:47 +0300 Subject: [PATCH 07/33] Refactored react plugin --- packages/react/src/main/FieldRenderer.ts | 18 ++++++------------ packages/react/src/main/useField.ts | 10 +++++----- packages/roqueform/src/main/utils.ts | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index e46f6af7..ed4792ae 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -1,5 +1,5 @@ import { createElement, Fragment, ReactElement, ReactNode, useEffect, useReducer, useRef } from 'react'; -import { callOrGet, Field, isEqual } from 'roqueform'; +import { callOrGet, Field } from 'roqueform'; /** * Properties of the {@link FieldRenderer} component. @@ -48,28 +48,22 @@ export function FieldRenderer(props: FieldRendererP handleChangeRef.current = props.onChange; useEffect(() => { - let prevValue: unknown; - - return field.subscribe(updatedField => { - const { value } = field; - - if (eagerlyUpdated || field === updatedField) { + return field.on('*', event => { + if (eagerlyUpdated || event.target === field) { rerender(); } - if (field.isTransient || isEqual(value, prevValue)) { + if (field.isTransient || event.type !== 'valueChange') { return; } - prevValue = value; - const handleChange = handleChangeRef.current; if (typeof handleChange === 'function') { - handleChange(value); + handleChange(field.value); } }); }, [field, eagerlyUpdated]); - return createElement(Fragment, null, callOrGet(props.children, [field])); + return createElement(Fragment, null, callOrGet(props.children, field)); } function reduceCount(count: number): number { diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index a4fa9172..e2ec8e23 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -1,5 +1,5 @@ import { useContext, useRef } from 'react'; -import { callOrGet, createField, Field, Plugin } from 'roqueform'; +import { callOrGet, createField, Field, PluginCallback } from 'roqueform'; import { AccessorContext } from './AccessorContext'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types @@ -33,11 +33,11 @@ export function useField(initialValue: Value | (() => Value)): Field( initialValue: Value | (() => Value), - plugin: Plugin> -): Field & Plugin; + plugin: PluginCallback> +): Field; -export function useField(initialValue?: unknown, plugin?: Plugin) { +export function useField(initialValue?: unknown, plugin?: PluginCallback) { const accessor = useContext(AccessorContext); - return (useRef().current ||= createField(callOrGet(initialValue), plugin!, accessor)); + return (useRef().current ||= createField(callOrGet(initialValue, undefined), plugin!, accessor)); } diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index ade0155d..97a6486c 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,6 +1,6 @@ import { Event } from './typings'; -export function callOrGet(value: T | ((prevValue: T) => T), prevValue: T): T { +export function callOrGet(value: T | ((prevValue: A) => T), prevValue: A): T { return typeof value === 'function' ? (value as Function)(prevValue) : value; } From 790301158fcac5d12b2c5dd1327c4402a6fc1605 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 19:27:58 +0300 Subject: [PATCH 08/33] Refactored constraintValidationPlugin --- .../src/main/constraintValidationPlugin.ts | 222 ++++++++---------- packages/ref-plugin/src/main/refPlugin.ts | 2 +- packages/reset-plugin/src/main/resetPlugin.ts | 4 +- packages/roqueform/src/main/typings.ts | 8 +- packages/roqueform/src/main/utils.ts | 6 +- .../roqueform/src/main/validationPlugin.ts | 18 +- .../src/main/scrollToErrorPlugin.ts | 2 +- .../src/main/createElementValueAccessor.ts | 9 +- .../src/main/uncontrolledPlugin.ts | 141 +++++++---- 9 files changed, 214 insertions(+), 198 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index bbfd75d8..b3580e76 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -1,18 +1,33 @@ -import { Field, Plugin } from 'roqueform'; +import { dispatchEvents, Field, FieldEvent, PluginCallback } from 'roqueform'; /** * The plugin added to fields by the {@link constraintValidationPlugin}. */ export interface ConstraintValidationPlugin { + /** + * @internal + */ + ['__plugin']: unknown; + + /** + * @internal + */ + value: unknown; + /** * An error associated with the field, or `null` if there's no error. */ - readonly error: string | null; + error: string | null; + + /** + * The element which provides the validity status. + */ + element: Element | null; /** * `true` if the field or any of its derived fields have an associated error, or `false` otherwise. */ - readonly isInvalid: boolean; + isInvalid: boolean; /** * The [validity state](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState), or `null` if there's no @@ -21,7 +36,7 @@ export interface ConstraintValidationPlugin { readonly validity: ValidityState | null; /** - * The callback that associates the field with the DOM element. + * Associates the field with the DOM element. */ refCallback(element: Element | null): void; @@ -62,115 +77,71 @@ export interface ConstraintValidationPlugin { * @returns `true` if a field doesn't have an error, or `false` otherwise. */ reportValidity(): boolean; + + /** + * Subscribes the listener field validity change events. An {@link error} would contain the associated error. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on(eventType: 'validityChange', listener: (event: FieldEvent) => void): () => void; } /** * Enhances fields with Constraint Validation API methods. */ -export function constraintValidationPlugin(): Plugin { - let controllerMap: WeakMap; - - return (field, _accessor, notify) => { - controllerMap ||= new WeakMap(); - - if (controllerMap.has(field)) { - return; - } - - const notifyAncestors = () => { - for ( - let invalid = false, ancestor: FieldController | null = controller; - ancestor !== null; - ancestor = ancestor._parent - ) { - invalid ||= isInvalid(controller); - - if (ancestor._isInvalid === invalid) { - break; - } - ancestor._isInvalid = invalid; - ancestor._notify(); - } - }; - - const controller: FieldController = { - _parent: null, - _children: null, - _field: field, - _element: null, - _validity: null, - _isInvalid: false, - _error: '', - _notify: notify, - _notifyAncestors: notifyAncestors, - }; - - controllerMap.set(field, controller); - - if (field.parent !== null) { - const parent = controllerMap.get(field.parent)!; - - controller._parent = parent; - - (parent._children ||= []).push(controller); - } - +export function constraintValidationPlugin(): PluginCallback { + return field => { const { refCallback } = field; - const listener = (event: Event): void => { - if (controller._element !== null && controller._element === event.target) { - notifyAncestors(); + const changeListener = (event: Event): void => { + if (field.element === event.target && isValidatable(event.target as Element)) { + dispatchEvents(setInvalid(field, [])); } }; Object.defineProperties(field, { - error: { enumerable: true, get: () => getError(controller) }, - isInvalid: { enumerable: true, get: () => isInvalid(controller) }, - validity: { enumerable: true, get: () => controller._validity }, + validity: { enumerable: true, get: () => (isValidatable(field.element) ? field.element.validity : null) }, }); field.refCallback = element => { - const { _element } = controller; - - if (_element === element) { + if (field.element === element) { refCallback?.(element); return; } - if (_element !== null) { - _element.removeEventListener('input', listener); - _element.removeEventListener('change', listener); - _element.removeEventListener('invalid', listener); + if (field.element !== null) { + field.element.removeEventListener('input', changeListener); + field.element.removeEventListener('change', changeListener); + field.element.removeEventListener('invalid', changeListener); - controller._element = controller._validity = null; - controller._error = ''; + field.element = field.error = null; } if (isValidatable(element)) { - element.addEventListener('input', listener); - element.addEventListener('change', listener); - element.addEventListener('invalid', listener); - - controller._element = element; - controller._validity = element.validity; + element.addEventListener('input', changeListener); + element.addEventListener('change', changeListener); + element.addEventListener('invalid', changeListener); } + field.element = element; refCallback?.(element); - notifyAncestors(); + dispatchEvents(setInvalid(field, [])); }; field.setError = error => { - setError(controller, error); + setError(field, error); }; field.deleteError = () => { - setError(controller, ''); + setError(field, null); }; field.clearErrors = () => { - clearErrors(controller); + clearErrors(field); }; - field.reportValidity = () => reportValidity(controller); + field.reportValidity = () => reportValidity(field); }; } @@ -184,88 +155,79 @@ type ValidatableElement = | HTMLSelectElement | HTMLTextAreaElement; -interface FieldController { - _parent: FieldController | null; - _children: FieldController[] | null; - _field: Field; - _element: ValidatableElement | null; - _validity: ValidityState | null; - - /** - * The invalid status for which the field was notified the last time. - */ - _isInvalid: boolean; - - /** - * An error that is used if the field doesn't have an associated element. - */ - _error: string; - - /** - * Synchronously notifies listeners of the field. - */ - _notify: () => void; - - /** - * Notifies the field and its ancestors about changes. - */ - _notifyAncestors: () => void; +function setInvalid( + field: Field, + events: FieldEvent[] +): FieldEvent[] { + for ( + let invalid = false, ancestor: Field | null = field; + ancestor !== null; + ancestor = ancestor.parent + ) { + invalid ||= isInvalid(field); + + if (ancestor.isInvalid === invalid) { + break; + } + ancestor.isInvalid = invalid; + events.push({ type: 'validityChange', target: field, currentTarget: ancestor }); + } + return events; } -/** - * Sets a validation error to the field and notifies it. - */ -function setError(controller: FieldController, error: string): void { - const { _element } = controller; +function setError(field: Field, error: string | null | undefined): void { + if (isValidatable(field.element)) { + error ||= ''; - if (_element !== null) { - if (_element.validationMessage !== error) { - _element.setCustomValidity(error); - controller._notifyAncestors(); + if (field.element.validationMessage !== error) { + field.element.setCustomValidity(error); + dispatchEvents(setInvalid(field, [])); } return; } - if (controller._error !== error) { - controller._error = error; - controller._notifyAncestors(); + error ||= null; + + if (field.error !== error) { + field.error = error; + dispatchEvents(setInvalid(field, [])); } } -function getError(controller: FieldController): string | null { - return (controller._element !== null ? controller._element.validationMessage : controller._error) || null; +function getError(field: Field): string | null { + return isValidatable(field.element) ? field.element.validationMessage || null : field.error; } -function clearErrors(controller: FieldController): void { - setError(controller, ''); +function clearErrors(field: Field): void { + setError(field, null); - if (controller._children !== null) { - for (const child of controller._children) { + if (field.children !== null) { + for (const child of field.children) { clearErrors(child); } } } -function isInvalid(controller: FieldController): boolean { - if (controller._children !== null) { - for (const child of controller._children) { +function isInvalid(field: Field): boolean { + if (field.children !== null) { + for (const child of field.children) { if (isInvalid(child)) { return true; } } } - return getError(controller) !== null; + return getError(field) !== null; } -function reportValidity(controller: FieldController): boolean { - if (controller._children !== null) { - for (const child of controller._children) { +function reportValidity(field: Field): boolean { + if (field.children !== null) { + for (const child of field.children) { if (!reportValidity(child)) { return false; } } } - return controller._element !== null ? controller._element.reportValidity() : getError(controller) === null; + return isValidatable(field.element) ? field.element.reportValidity() : getError(field) === null; } function isValidatable(element: Element | null): element is ValidatableElement { diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index 3ed621c0..d2057c68 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -10,7 +10,7 @@ export interface RefPlugin { element: Element | null; /** - * The callback that associates the field with the {@link element DOM element}. + * Associates the field with the {@link element DOM element}. */ refCallback(element: Element | null): void; diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 77b7161f..c5c276ea 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,4 +1,4 @@ -import { dispatchEvents, Event, Field, isEqual, PluginCallback } from 'roqueform'; +import { dispatchEvents, FieldEvent, Field, isEqual, PluginCallback } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** @@ -59,7 +59,7 @@ export interface ResetPlugin { * @template Plugin The plugin added to the field. * @template Value The field value. */ -export interface InitialValueChangeEvent extends Event { +export interface InitialValueChangeEvent extends FieldEvent { type: 'initialValueChange'; /** diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 2b87614e..94900dff 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -68,7 +68,7 @@ interface FieldController { /** * The map from an event type to an array of associated listeners, or `null` if no listeners were added. */ - ['listeners']: { [eventType: string]: Array<(event: Event) => void> } | null; + ['listeners']: { [eventType: string]: Array<(event: FieldEvent) => void> } | null; /** * The accessor that reads the field value from the value of the parent fields, and updates parent value. @@ -119,7 +119,7 @@ interface FieldController { * @param listener The listener that would be triggered. * @returns The callback to unsubscribe the listener. */ - on(eventType: '*', listener: (event: Event) => void): () => void; + on(eventType: '*', listener: (event: FieldEvent) => void): () => void; /** * Subscribes the listener to field value change events. @@ -137,7 +137,7 @@ interface FieldController { * @template Plugin The plugin added to the field. * @template Value The field value. */ -export interface Event { +export interface FieldEvent { /** * The type of the event. */ @@ -160,7 +160,7 @@ export interface Event { * @template Plugin The plugin added to the field. * @template Value The field value. */ -export interface ValueChangeEvent extends Event { +export interface ValueChangeEvent extends FieldEvent { type: 'valueChange'; /** diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 97a6486c..6ec67fd4 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,4 +1,4 @@ -import { Event } from './typings'; +import { FieldEvent } from './typings'; export function callOrGet(value: T | ((prevValue: A) => T), prevValue: A): T { return typeof value === 'function' ? (value as Function)(prevValue) : value; @@ -15,7 +15,7 @@ export function isEqual(a: unknown, b: unknown): boolean { return a === b || (a !== a && b !== b); } -export function dispatchEvents(events: readonly Event[]): void { +export function dispatchEvents>(events: readonly E[]): void { for (const event of events) { const { listeners } = event.currentTarget; @@ -26,7 +26,7 @@ export function dispatchEvents(events: readonly Event[]): void { } } -function callAll(listeners: Array<(event: Event) => void> | undefined, event: Event): void { +function callAll(listeners: Array<(event: FieldEvent) => void> | undefined, event: FieldEvent): void { if (listeners !== undefined) { for (const listener of listeners) { try { diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index bcaa74ee..304a33f0 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,4 +1,4 @@ -import { Event, Field, PluginCallback } from './typings'; +import { FieldEvent, Field, PluginCallback } from './typings'; import { dispatchEvents, isEqual } from './utils'; const EVENT_CHANGE = 'validityChange'; @@ -137,29 +137,29 @@ export interface ValidationPlugin { * @param listener The listener that would be triggered. * @returns The callback to unsubscribe the listener. */ - on(eventType: 'validityChange', listener: (event: Event) => void): () => void; + on(eventType: 'validityChange', listener: (event: FieldEvent) => void): () => void; /** * Subscribes the listener to validation start events. The event is triggered for all fields that are going to be * validated. The {@link FieldController.value current value} of the field is the one that is being validated. - * {@link Event.target} points to the field where validation was triggered. + * {@link FieldEvent.target} points to the field where validation was triggered. * * @param eventType The type of the event. * @param listener The listener that would be triggered. * @returns The callback to unsubscribe the listener. */ - on(eventType: 'validationStart', listener: (event: Event) => void): () => void; + on(eventType: 'validationStart', listener: (event: FieldEvent) => void): () => void; /** * Subscribes the listener to validation start end events. The event is triggered for all fields that were validated. - * {@link Event.target} points to the field where validation was triggered. Check {@link isInvalid} to detect the + * {@link FieldEvent.target} points to the field where validation was triggered. Check {@link isInvalid} to detect the * actual validity status. * * @param eventType The type of the event. * @param listener The listener that would be triggered. * @returns The callback to unsubscribe the listener. */ - on(eventType: 'validationEnd', listener: (event: Event) => void): () => void; + on(eventType: 'validationEnd', listener: (event: FieldEvent) => void): () => void; /** * Associates a validation error with the field and notifies the subscribers. Use this method in @@ -179,7 +179,7 @@ export interface ValidationPlugin { */ export interface Validator { /** - * The callback that applies validation rules to a field. + * Applies validation rules to a field. * * Set {@link ValidationPlugin.setValidationError validation errors} to invalid fields during validation. * @@ -189,7 +189,7 @@ export interface Validator { validate(field: Field>, options: Options | undefined): void; /** - * The callback that applies validation rules to a field. + * Applies validation rules to a field. * * Check that {@link Validation.abortController validation isn't aborted} before * {@link ValidationPlugin.setValidationError setting a validation error}, otherwise stop validation as soon as @@ -281,7 +281,7 @@ export function validationPlugin( }; } -type ValidationEvent = Event; +type ValidationEvent = FieldEvent; function setError( field: Field, diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 7057ecf1..927f2c5f 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -22,7 +22,7 @@ export interface ScrollToErrorPlugin { element: Element | null; /** - * The callback that associates the field with the DOM element. + * Associates the field with the DOM element. */ refCallback(element: Element | null): void; diff --git a/packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts b/packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts index f3925ddf..69d50397 100644 --- a/packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts +++ b/packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts @@ -125,7 +125,9 @@ export interface ElementValueAccessorOptions { * - Others → The _value_ attribute, or `null` if element doesn't support it; * - `null`, `undefined`, `NaN` and non-finite numbers are coerced to an empty string and written to _value_ attribute. */ -export function createElementValueAccessor(options?: ElementValueAccessorOptions): ElementValueAccessor { +export function createElementValueAccessor(options: ElementValueAccessorOptions = {}): ElementValueAccessor { + const { checkboxFormat, dateFormat, timeFormat } = options; + const get: ElementValueAccessor['get'] = elements => { const element = elements[0]; const { type, valueAsNumber } = element; @@ -135,8 +137,6 @@ export function createElementValueAccessor(options?: ElementValueAccessorOptions } if (type === 'checkbox') { - const checkboxFormat = options?.checkboxFormat; - if (elements.length === 1 && checkboxFormat !== 'booleanArray' && checkboxFormat !== 'valueArray') { return checkboxFormat !== 'value' ? element.checked : element.checked ? element.value : null; } @@ -177,7 +177,6 @@ export function createElementValueAccessor(options?: ElementValueAccessorOptions } const date = element.valueAsDate || new Date(valueAsNumber); - const dateFormat = options?.dateFormat; // prettier-ignore return ( @@ -191,7 +190,7 @@ export function createElementValueAccessor(options?: ElementValueAccessorOptions } if (type === 'time') { - return valueAsNumber !== valueAsNumber ? null : options?.timeFormat === 'number' ? valueAsNumber : element.value; + return valueAsNumber !== valueAsNumber ? null : timeFormat === 'number' ? valueAsNumber : element.value; } if (type === 'image') { return element.src; diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index 36655c04..b880ddd7 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -1,7 +1,9 @@ -import { Plugin } from 'roqueform'; +import { dispatchEvents, Field, FieldEvent, PluginCallback } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; +const EVENT_TRACK = 'trackedElementsChange'; + /** * The default value accessor. */ @@ -12,83 +14,133 @@ const elementValueAccessor = createElementValueAccessor(); */ export interface UncontrolledPlugin { /** - * The callback that associates the field with the DOM element. + * @internal */ - refCallback(element: Element | null): void; + ['__plugin']: unknown; /** - * Overrides the element value accessor for the field. + * @internal + */ + ['value']: unknown; + + /** + * The array of that are used to derive the field value. Update this array by calling {@link track} method. Elements + * are watched by {@link MutationObserver} and removed from this array when they are removed from the DOM. + */ + ['trackedElements']: Element[]; + + /** + * The accessor that reads and writes field value from and to {@link trackedElements tracked elements}. + */ + ['elementValueAccessor']: ElementValueAccessor; + + /** + * The adds the DOM element to the list of tracked elements. + * + * @param element The element to track. No-op if the element is `null` or not connected to the DOM. + */ + track(element: Element | null): void; + + /** + * Overrides {@link elementValueAccessor the element value accessor} for the field. * * @param accessor The accessor to use for this filed. */ - setAccessor(accessor: Partial): this; + setElementValueAccessor(accessor: Partial): this; + + /** + * Subscribes the listener to additions and deletions in of {@link trackedElements tracked elements}. + * + * @param eventType The type of the event. + * @param listener The listener that would be triggered. + * @returns The callback to unsubscribe the listener. + */ + on( + eventType: 'trackedElementsChange', + listener: (event: FieldEvent) => void + ): () => void; + + /** + * Associates the field with {@link element the DOM element}. This method is usually exposed by plugins that use DOM + * element references. This method is invoked when {@link trackedElements the first tracked element} is changed. + */ + ['refCallback']?(element: Element | null): void; +} + +export interface TrackedElementsChangeEvent extends FieldEvent { + type: 'trackedElementsChange'; + + /** + * The element that was added or removed from {@link trackedElements tracked elements}. + */ + element: Element; } /** * Updates field value when the DOM element value is changed and vice versa. * - * @param accessor The accessor that reads and writes value to and from the DOM elements managed by the filed. + * @param defaultAccessor The accessor that reads and writes value to and from the DOM elements managed by the filed. */ -export function uncontrolledPlugin(accessor = elementValueAccessor): Plugin { +export function uncontrolledPlugin(defaultAccessor = elementValueAccessor): PluginCallback { return field => { - const { refCallback } = field; - - let elements: Element[] = []; - let targetElement: Element | null = null; + field.trackedElements = []; + field.elementValueAccessor = defaultAccessor; - let getElementValue = accessor.get; - let setElementValue = accessor.set; + const mutationObserver = new MutationObserver(mutations => { + const events: TrackedElementsChangeEvent[] = []; + const { trackedElements } = field; + const [element] = trackedElements; - const mutationObserver = new MutationObserver((mutations: MutationRecord[]) => { for (const mutation of mutations) { for (let i = 0; i < mutation.removedNodes.length; ++i) { - const j = elements.indexOf(mutation.removedNodes.item(i) as Element); - if (j === -1) { + const elementIndex = trackedElements.indexOf(mutation.removedNodes.item(i) as Element); + + if (elementIndex === -1) { continue; } + const element = trackedElements[elementIndex]; + element.removeEventListener('input', changeListener); + element.removeEventListener('change', changeListener); - elements[j].removeEventListener('input', changeListener); - elements[j].removeEventListener('change', changeListener); - - elements.splice(j, 1); + trackedElements.splice(elementIndex, 1); + events.push({ type: EVENT_TRACK, target: field as Field, currentTarget: field as Field, element }); } } - if (elements.length === 0) { + if (trackedElements.length === 0) { mutationObserver.disconnect(); - targetElement = null; - refCallback?.(targetElement); - return; + field.refCallback?.(null); + } else if (element !== trackedElements[0]) { + field.refCallback?.(trackedElements[0]); } - if (targetElement !== elements[0]) { - targetElement = elements[0]; - refCallback?.(targetElement); - } + dispatchEvents(events); }); const changeListener = (event: Event): void => { let value; if ( - elements.indexOf(event.target as Element) !== -1 && - !isDeepEqual((value = getElementValue(elements.slice(0))), field.value) + field.trackedElements.indexOf(event.target as Element) !== -1 && + !isDeepEqual((value = field.elementValueAccessor.get(field.trackedElements)), field.value) ) { field.setValue(value); } }; - field.subscribe(() => { - if (elements.length !== 0) { - setElementValue(elements.slice(0), field.value); + field.on('valueChange', () => { + if (field.trackedElements.length !== 0) { + field.elementValueAccessor.set(field.trackedElements, field.value); } }); - field.refCallback = element => { + field.track = element => { + const { trackedElements } = field; + if ( element === null || !(element instanceof Element) || !element.isConnected || - elements.indexOf(element) !== -1 + trackedElements.indexOf(element) !== -1 ) { return; } @@ -98,19 +150,22 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): Plugin { - getElementValue = accessor.get || getElementValue; - setElementValue = accessor.set || setElementValue; + field.setElementValueAccessor = accessor => { + field.elementValueAccessor = { + get: accessor.get || defaultAccessor.get, + set: accessor.set || defaultAccessor.set, + }; return field; }; }; From c99c5c42edb50697e6aae7545571465ff022d8ba Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Wed, 8 Nov 2023 19:47:14 +0300 Subject: [PATCH 09/33] Refactored zodPlugin --- .../doubter-plugin/src/main/doubterPlugin.ts | 6 +- packages/zod-plugin/src/main/zodPlugin.ts | 87 ++++++++++--------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index a04360ef..7a516478 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -47,14 +47,14 @@ const doubterValidator: Validator = { validate(field, options) { const { validation, shape } = field as unknown as Field; - endValidation(validation!, shape.try(field.value, Object.assign({ verbose: true }, options))); + applyResult(validation!, shape.try(field.value, Object.assign({ verbose: true }, options))); }, validateAsync(field, options) { const { validation, shape } = field as unknown as Field; return shape.tryAsync(field.value, Object.assign({ verbose: true }, options)).then(result => { - endValidation(validation!, result); + applyResult(validation!, result); }); }, }; @@ -65,7 +65,7 @@ function setPath(field: Field, issue: Issue): void { } } -function endValidation(validation: Validation, result: Err | Ok): void { +function applyResult(validation: Validation, result: Err | Ok): void { if (result.ok) { return; } diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index 8cb940d0..7f820c72 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -1,85 +1,86 @@ -import { ParseParams, ZodErrorMap, ZodIssue, ZodIssueCode, ZodType, ZodTypeAny } from 'zod'; -import { Accessor, Field, Plugin, ValidationPlugin, validationPlugin } from 'roqueform'; +import { ParseParams, SafeParseReturnType, ZodIssue, ZodIssueCode, ZodType, ZodTypeAny } from 'zod'; +import { Field, PluginCallback, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; /** * The plugin added to fields by the {@link zodPlugin}. */ export interface ZodPlugin extends ValidationPlugin> { - setError(error: ZodIssue | string): void; + /** + * The Zod validation schema of the root value. + */ + ['schema']: ZodTypeAny; + + setError(error: ZodIssue | string | null | undefined): void; } /** * Enhances fields with validation methods powered by [Zod](https://zod.dev/). * - * @param type The shape that parses the field value. - * @param errorMap [The Zod error customizer.](https://github.com/colinhacks/zod/blob/master/ERROR_HANDLING.md#customizing-errors-with-zoderrormap) + * @param schema The shape that parses the field value. * @template Value The root field value. * @returns The validation plugin. */ -export function zodPlugin(type: ZodType, errorMap?: ZodErrorMap): Plugin { - let plugin: Plugin; +export function zodPlugin(schema: ZodType): PluginCallback { + let plugin: PluginCallback; + + return field => { + (plugin ||= validationPlugin(zodValidator))(field); - return (field, accessor, notify) => { - (plugin ||= createValidationPlugin(type, errorMap, accessor))(field, accessor, notify); + field.schema = schema; const { setError } = field; field.setError = error => { - setError(typeof error !== 'string' ? error : { code: ZodIssueCode.custom, path: getPath(field), message: error }); + if (typeof error === 'string') { + error = { code: ZodIssueCode.custom, path: getPath(field), message: error }; + } + setError(error); }; }; } -function createValidationPlugin(type: ZodTypeAny, errorMap: ZodErrorMap | undefined, accessor: Accessor) { - return validationPlugin>({ - validate(field, setError, options) { - options = Object.assign({ errorMap }, options); +const zodValidator: Validator> = { + validate(field, options) { + applyResult(field, (field as unknown as Field).schema.safeParse(getRootValue(field), options)); + }, - const result = type.safeParse(getValue(field, accessor), options); - - if (!result.success) { - setIssues(field, result.error.issues, setError); - } - }, - - validateAsync(field, setError, options) { - options = Object.assign({ errorMap }, options); - - return type.safeParseAsync(getValue(field, accessor), options).then(result => { - if (!result.success) { - setIssues(field, result.error.issues, setError); - } - }); - }, - }); -} + validateAsync(field, options) { + return (field as unknown as Field).schema.safeParseAsync(getRootValue(field), options).then(result => { + applyResult(field, result); + }); + }, +}; -/** - * Returns the value of the root field that contains a transient value of the target field. - */ -function getValue(field: Field, accessor: Accessor): unknown { +function getRootValue(field: Field): unknown { let value = field.value; + let transient = false; while (field.parent !== null) { - value = accessor.set(field.parent.value, field.key, value); + transient ||= field.isTransient; + value = transient ? field.accessor.set(field.parent.value, field.key, value) : field.parent.value; field = field.parent; } return value; } -function getPath(field: Field): Array { - const path: Array = []; - +function getPath(field: Field): any[] { + const path = []; for (let ancestor = field; ancestor.parent !== null; ancestor = ancestor.parent) { path.unshift(ancestor.key); } return path; } -function setIssues(field: Field, issues: ZodIssue[], setError: (field: Field, error: ZodIssue) => void): void { +function applyResult(field: Field, result: SafeParseReturnType): void { + const { validation } = field; + + if (validation === null || result.success) { + return; + } + let prefix = getPath(field); - issues: for (const issue of issues) { + issues: for (const issue of result.error.issues) { const { path } = issue; let targetField = field; @@ -95,6 +96,6 @@ function setIssues(field: Field, issues: ZodIssue[], setError: (field: Field, er for (let i = prefix.length; i < path.length; ++i) { targetField = targetField.at(path[i]); } - setError(targetField, issue); + field.setValidationError(validation, issue); } } From a3fdb5fc8fc8e37ec8e3b408f9028eacece3222f Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Thu, 9 Nov 2023 17:08:40 +0300 Subject: [PATCH 10/33] Better naming --- README.md | 16 +- .../constraint-validation-plugin/README.md | 2 +- .../src/main/constraintValidationPlugin.ts | 263 ++++++++++++------ .../src/main/index.ts | 2 +- .../test/constraintValidationPlugin.test.ts | 66 ++--- .../doubter-plugin/src/main/doubterPlugin.ts | 56 ++-- packages/doubter-plugin/src/main/index.ts | 2 +- packages/react/src/main/FieldRenderer.ts | 22 +- packages/react/src/main/index.ts | 2 +- packages/react/src/main/useField.ts | 8 +- packages/ref-plugin/README.md | 2 +- packages/ref-plugin/src/main/index.ts | 2 +- packages/ref-plugin/src/main/refPlugin.ts | 14 +- .../ref-plugin/src/test/refPlugin.test.ts | 16 +- packages/reset-plugin/src/main/index.ts | 2 +- packages/reset-plugin/src/main/resetPlugin.ts | 62 +---- .../reset-plugin/src/test/resetPlugin.test.ts | 6 +- packages/roqueform/src/main/composePlugins.ts | 70 ++--- packages/roqueform/src/main/createField.ts | 42 +-- .../roqueform/src/main/naturalAccessor.ts | 13 +- packages/roqueform/src/main/typings.ts | 199 ++++++++----- packages/roqueform/src/main/utils.ts | 20 +- .../roqueform/src/main/validationPlugin.ts | 172 ++++++------ .../roqueform/src/test/createField.test.ts | 100 +++---- .../src/test/validationPlugin.test.tsx | 72 ++--- packages/scroll-to-error-plugin/README.md | 2 +- .../scroll-to-error-plugin/src/main/index.ts | 2 +- .../src/main/scrollToErrorPlugin.ts | 21 +- .../src/test/scrollToErrorPlugin.test.tsx | 8 +- packages/uncontrolled-plugin/README.md | 16 +- .../uncontrolled-plugin/src/main/index.ts | 2 +- .../src/main/uncontrolledPlugin.ts | 124 ++++----- .../src/test/uncontrolledPlugin.test.ts | 86 +++--- packages/zod-plugin/src/main/index.ts | 2 +- packages/zod-plugin/src/main/zodPlugin.ts | 71 ++--- tsconfig.typedoc.json | 6 + typedoc.json | 6 +- 37 files changed, 853 insertions(+), 724 deletions(-) create mode 100644 tsconfig.typedoc.json diff --git a/README.md b/README.md index 2b4d57c3..2e2d3ab4 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ nameField.value; # Subscriptions -You can subscribe a listener to a field updates. The returned callback would unsubscribe the listener. +You can subscribe a subscriber to a field updates. The returned callback would unsubscribe the subscriber. ```ts const unsubscribe = planetsField.subscribe((updatedField, currentField) => { @@ -190,32 +190,32 @@ of its ancestor fields.
currentField
-The field to which the listener is subscribed. In this example it is `planetsField`. +The field to which the subscriber is subscribed. In this example it is `planetsField`.
Listeners are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin). -The root field and all derived fields are updated before listeners are called, so it's safe to read field values in a -listener. +The root field and all derived fields are updated before subscribers are called, so it's safe to read field values in a +subscriber. Fields use [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison to detect that the value has changed. ```ts -planetsField.at(0).at('name').subscribe(listener); +planetsField.at(0).at('name').subscribe(subscriber); -// ✅ The listener is called +// ✅ The subscriber is called planetsField.at(0).at('name').setValue('Mercury'); -// 🚫 Value is unchanged, the listener isn't called +// 🚫 Value is unchanged, the subscriber isn't called planetsField.at(0).setValue({ name: 'Mercury' }); ``` # Transient updates When you call [`setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue) on a field -then listeners of its ancestors and its updated derived fields are triggered. To manually control the update propagation +then subscribers of its ancestors and its updated derived fields are triggered. To manually control the update propagation to fields ancestors, you can use transient updates. When a value of a derived field is set transiently, values of its ancestors _aren't_ immediately updated. diff --git a/packages/constraint-validation-plugin/README.md b/packages/constraint-validation-plugin/README.md index 33b09669..f581ff58 100644 --- a/packages/constraint-validation-plugin/README.md +++ b/packages/constraint-validation-plugin/README.md @@ -29,7 +29,7 @@ export const App = () => { { nameField.setValue(event.target.value); diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index b3580e76..a049f7cf 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -1,71 +1,70 @@ -import { dispatchEvents, Field, FieldEvent, PluginCallback } from 'roqueform'; +import { dispatchEvents, Event as Event_, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform'; + +const EVENT_CHANGE_ERROR = 'change:error'; /** * The plugin added to fields by the {@link constraintValidationPlugin}. */ export interface ConstraintValidationPlugin { - /** - * @internal - */ - ['__plugin']: unknown; - - /** - * @internal - */ - value: unknown; - /** * An error associated with the field, or `null` if there's no error. */ error: string | null; /** - * The element which provides the validity status. + * The DOM element associated with the field, or `null` if there's no associated element. */ element: Element | null; /** - * `true` if the field or any of its derived fields have an associated error, or `false` otherwise. + * `true` if the field or any of its derived fields have {@link error an associated error}, or `false` otherwise. */ - isInvalid: boolean; + readonly isInvalid: boolean; /** * The [validity state](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState), or `null` if there's no * associated element, or it doesn't support Constraint Validation API. */ - readonly validity: ValidityState | null; + validity: ValidityState | null; /** - * Associates the field with the DOM element. + * The total number of errors associated with this field and its child fields. + * + * @protected */ - refCallback(element: Element | null): void; + ['errorCount']: number; /** - * Associates an error with the field. Calls - * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity setCustomValidity} - * if the field has an associated element. + * The origin of the associated error: + * - 0 if there's no associated error. + * - 1 if an error was set using Constraint Validation API; + * - 2 if an error was set using {@link ValidationPlugin.setError}; * - * If a field has an associated element that doesn't satisfy validation constraints this method is no-op. - * - * @param error The error to set. + * @protected */ - setError(error: string): void; + ['errorOrigin']: 0 | 1 | 2; /** - * Deletes an error associated with this field. Calls - * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity setCustomValidity} - * if the field has an associated element. + * Associates the field with the {@link element DOM element}. + */ + ref(element: Element | null): void; + + /** + * Associates an error with the field. * - * If a field has an associated element that doesn't satisfy validation constraints this method is no-op. + * @param error The error to set. If `null` or `undefined` then an error is deleted. + */ + setError(error: string | null | undefined): void; + + /** + * Deletes an error associated with this field. If this field has {@link element an associated element} that supports + * [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation) and the + * element has non-empty `validationMessage` then this message would be immediately set as an error for this field. */ deleteError(): void; /** - * Recursively deletes errors associated with this field and all of its derived fields. Calls - * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity setCustomValidity} - * if the field has an associated element. - * - * If a field has an associated element that doesn't satisfy validation constraints this method is no-op. + * Recursively deletes errors associated with this field and all of its derived fields. */ clearErrors(): void; @@ -79,69 +78,103 @@ export interface ConstraintValidationPlugin { reportValidity(): boolean; /** - * Subscribes the listener field validity change events. An {@link error} would contain the associated error. + * Returns all errors associated with this field and its child fields. + */ + getErrors(): string[]; + + /** + * Returns all fields that have an error. + */ + getInvalidFields(): Field>[]; + + /** + * Subscribes to {@link error an associated error} changes of this field or any of its descendants. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. + * @see {@link error} + * @see {@link isInvalid} */ - on(eventType: 'validityChange', listener: (event: FieldEvent) => void): () => void; + on(eventType: 'change:error', subscriber: Subscriber): Unsubscribe; } /** * Enhances fields with Constraint Validation API methods. */ -export function constraintValidationPlugin(): PluginCallback { +export function constraintValidationPlugin(): PluginInjector { return field => { - const { refCallback } = field; + field.error = field.element = field.validity = null; + field.errorCount = 0; + + Object.defineProperties(field, { + isInvalid: { get: () => field.errorCount !== 0 }, + }); const changeListener = (event: Event): void => { - if (field.element === event.target && isValidatable(event.target as Element)) { - dispatchEvents(setInvalid(field, [])); + if (field.element === event.target && isValidatable(field.element)) { + dispatchEvents(setError(field, field.element.validationMessage, 1, [])); } }; - Object.defineProperties(field, { - validity: { enumerable: true, get: () => (isValidatable(field.element) ? field.element.validity : null) }, - }); + const { ref } = field; - field.refCallback = element => { + field.ref = element => { if (field.element === element) { - refCallback?.(element); + // Same element + ref?.(element); return; } if (field.element !== null) { + // Disconnect current element field.element.removeEventListener('input', changeListener); field.element.removeEventListener('change', changeListener); field.element.removeEventListener('invalid', changeListener); field.element = field.error = null; } + + field.element = element; + field.validity = null; + + const events: Event_[] = []; + if (isValidatable(element)) { + // Connect new element element.addEventListener('input', changeListener); element.addEventListener('change', changeListener); element.addEventListener('invalid', changeListener); + + setError(field, element.validationMessage, 1, events); + + field.validity = element.validity; } - field.element = element; - refCallback?.(element); - dispatchEvents(setInvalid(field, [])); + try { + ref?.(element); + } finally { + dispatchEvents(events); + } }; field.setError = error => { - setError(field, error); + dispatchEvents(setError(field, error, 2, [])); }; field.deleteError = () => { - setError(field, null); + dispatchEvents(deleteError(field, 2, [])); }; field.clearErrors = () => { - clearErrors(field); + dispatchEvents(clearErrors(field, [])); }; field.reportValidity = () => reportValidity(field); + + field.getErrors = () => getErrors(getInvalidFields(field, [])); + + field.getInvalidFields = () => getInvalidFields(field, []); }; } @@ -155,79 +188,127 @@ type ValidatableElement = | HTMLSelectElement | HTMLTextAreaElement; -function setInvalid( +function setError( field: Field, - events: FieldEvent[] -): FieldEvent[] { - for ( - let invalid = false, ancestor: Field | null = field; - ancestor !== null; - ancestor = ancestor.parent - ) { - invalid ||= isInvalid(field); - - if (ancestor.isInvalid === invalid) { - break; + error: string | null | undefined, + errorOrigin: 1 | 2, + events: Event_[] +): Event_[] { + if (error === null || error === undefined || error.length === 0) { + return deleteError(field, errorOrigin, events); + } + + const originalError = field.error; + + if (field.errorOrigin > errorOrigin || (originalError === error && field.errorOrigin === errorOrigin)) { + return events; + } + + if (isValidatable(field.element)) { + field.element.setCustomValidity(error); + } + + field.error = error; + field.errorOrigin = errorOrigin; + + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + + if (originalError !== null) { + return events; + } + + field.errorCount++; + + for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { + if (ancestor.errorCount++ === 0) { + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); } - ancestor.isInvalid = invalid; - events.push({ type: 'validityChange', target: field, currentTarget: ancestor }); } + return events; } -function setError(field: Field, error: string | null | undefined): void { +function deleteError(field: Field, errorOrigin: 1 | 2, events: Event_[]): Event_[] { + const originalError = field.error; + + if (field.errorOrigin > errorOrigin || originalError === null) { + return events; + } + if (isValidatable(field.element)) { - error ||= ''; + field.element.setCustomValidity(''); + + if (!field.element.validity.valid) { + field.errorOrigin = 1; - if (field.element.validationMessage !== error) { - field.element.setCustomValidity(error); - dispatchEvents(setInvalid(field, [])); + if (originalError !== (field.error = field.element.validationMessage)) { + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + } + return events; } - return; } - error ||= null; + field.error = null; + field.errorOrigin = 0; + field.errorCount--; + + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); - if (field.error !== error) { - field.error = error; - dispatchEvents(setInvalid(field, [])); + for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { + if (--ancestor.errorCount === 0) { + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); + } } -} -function getError(field: Field): string | null { - return isValidatable(field.element) ? field.element.validationMessage || null : field.error; + return events; } -function clearErrors(field: Field): void { - setError(field, null); +function clearErrors(field: Field, events: Event_[]): Event_[] { + deleteError(field, 2, events); if (field.children !== null) { for (const child of field.children) { - clearErrors(child); + clearErrors(child, events); } } + return events; } -function isInvalid(field: Field): boolean { +function reportValidity(field: Field): boolean { if (field.children !== null) { for (const child of field.children) { - if (isInvalid(child)) { - return true; + if (!reportValidity(child)) { + return false; } } } - return getError(field) !== null; + return isValidatable(field.element) ? field.element.reportValidity() : field.error === null; } -function reportValidity(field: Field): boolean { +function getInvalidFields( + field: Field, + batch: Field[] +): Field[] { + if (field.error !== null) { + batch.push(field); + } if (field.children !== null) { for (const child of field.children) { - if (!reportValidity(child)) { - return false; - } + getInvalidFields(child, batch); + } + } + return batch; +} + +function getErrors(batch: Field[]): string[] { + const errors = []; + + for (const field of batch) { + if (field.error !== null) { + errors.push(field.error); } } - return isValidatable(field.element) ? field.element.reportValidity() : getError(field) === null; + return errors; } function isValidatable(element: Element | null): element is ValidatableElement { diff --git a/packages/constraint-validation-plugin/src/main/index.ts b/packages/constraint-validation-plugin/src/main/index.ts index 050de48c..d081c50b 100644 --- a/packages/constraint-validation-plugin/src/main/index.ts +++ b/packages/constraint-validation-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/constraint-validation-plugin + * @module constraint-validation-plugin */ export * from './constraintValidationPlugin'; diff --git a/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts b/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts index 8aaa9c17..4373d6e8 100644 --- a/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts +++ b/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts @@ -26,10 +26,10 @@ describe('constraintValidationPlugin', () => { test('sets an error to the field that does not have an associated element', () => { const field = createField({ foo: 0 }, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setError('aaa'); @@ -39,17 +39,17 @@ describe('constraintValidationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe('aaa'); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); test('setting an error to the parent field does not affect the child field', () => { const field = createField({ foo: 0 }, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.setError('aaa'); @@ -59,45 +59,45 @@ describe('constraintValidationPlugin', () => { expect(field.at('foo').isInvalid).toBe(false); expect(field.at('foo').error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(0); }); test('does not notify the field if the same error is set', () => { const field = createField(0, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.setError('aaa'); field.setError('aaa'); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); }); test('deletes an error from the field', () => { const field = createField(0, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.setError('aaa'); field.deleteError(); expect(field.isInvalid).toBe(false); expect(field.error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); }); test('clears an error of a derived field', () => { const field = createField({ foo: 0 }, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setError('aaa'); @@ -146,7 +146,7 @@ describe('constraintValidationPlugin', () => { element.required = true; - field.at('foo').refCallback(element); + field.at('foo').ref(element); expect(field.isInvalid).toBe(true); expect(field.error).toBe(null); @@ -158,74 +158,74 @@ describe('constraintValidationPlugin', () => { test('deletes an error when a ref is changed', () => { const field = createField(0, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); element.required = true; - field.refCallback(element); + field.ref(element); expect(field.isInvalid).toBe(true); expect(field.error).toEqual('Constraints not satisfied'); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); - field.refCallback(null); + field.ref(null); expect(field.isInvalid).toBe(false); expect(field.error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); }); test('notifies the field when the value is changed', () => { const field = createField({ foo: 0 }, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); element.value = 'aaa'; element.required = true; expect(element.validationMessage).toBe(''); - expect(listenerMock).not.toHaveBeenCalled(); + expect(subscriberMock).not.toHaveBeenCalled(); - field.at('foo').refCallback(element); + field.at('foo').ref(element); - expect(listenerMock).toHaveBeenCalledTimes(0); + expect(subscriberMock).toHaveBeenCalledTimes(0); expect(field.at('foo').isInvalid).toBe(false); fireEvent.change(element, { target: { value: '' } }); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); test('does not notify an already invalid parent', () => { const field = createField({ foo: 0 }, constraintValidationPlugin()); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); element.value = 'aaa'; element.required = true; expect(element.validationMessage).toBe(''); - expect(listenerMock).not.toHaveBeenCalled(); + expect(subscriberMock).not.toHaveBeenCalled(); - field.at('foo').refCallback(element); + field.at('foo').ref(element); - expect(listenerMock).toHaveBeenCalledTimes(0); + expect(subscriberMock).toHaveBeenCalledTimes(0); expect(field.at('foo').isInvalid).toBe(false); fireEvent.change(element, { target: { value: '' } }); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index 7a516478..c3cc482e 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -1,5 +1,5 @@ import { Err, Issue, Ok, ParseOptions, Shape } from 'doubter'; -import { Field, PluginCallback, Validation, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; +import { AnyField, Field, PluginInjector, Validation, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; /** * The plugin added to fields by the {@link doubterPlugin}. @@ -8,8 +8,10 @@ export interface DoubterPlugin extends ValidationPlugin { /** * The shape that Doubter uses to validate {@link FieldController.value the field value}, or `null` if there's no * shape for this field. + * + * @protected */ - ['shape']: Shape; + ['shape']: Shape | null; setError(error: Issue | string | null | undefined): void; } @@ -20,25 +22,18 @@ export interface DoubterPlugin extends ValidationPlugin { * @param shape The shape that parses the field value. * @template Value The root field value. */ -export function doubterPlugin(shape: Shape): PluginCallback { +export function doubterPlugin(shape: Shape): PluginInjector { let plugin; return field => { (plugin ||= validationPlugin(doubterValidator))(field); - field.shape = field.parent === null ? shape : field.parent.shape?.at(field.key) || new Shape(); + field.shape = field.parent === null ? shape : field.parent.shape?.at(field.key) || null; const { setError } = field; field.setError = error => { - if (error !== null && error !== undefined) { - if (typeof error === 'string') { - error = { message: error }; - } - setPath(field, error); - error.input = field.value; - } - setError(error); + setError(typeof error === 'string' ? { message: error, path: getPath(field) } : error); }; }; } @@ -47,38 +42,53 @@ const doubterValidator: Validator = { validate(field, options) { const { validation, shape } = field as unknown as Field; - applyResult(validation!, shape.try(field.value, Object.assign({ verbose: true }, options))); + if (validation !== null && shape !== null) { + applyResult(validation, shape.try(field.value, Object.assign({ verbose: true }, options))); + } }, validateAsync(field, options) { const { validation, shape } = field as unknown as Field; - return shape.tryAsync(field.value, Object.assign({ verbose: true }, options)).then(result => { - applyResult(validation!, result); - }); + if (validation !== null && shape !== null) { + return shape.tryAsync(field.value, Object.assign({ verbose: true }, options)).then(result => { + applyResult(validation, result); + }); + } + + return Promise.resolve(); }, }; -function setPath(field: Field, issue: Issue): void { - for (let ancestor = field; ancestor.parent !== null; ancestor = ancestor.parent) { - (issue.path ||= []).unshift(ancestor.key); +function getPath(field: AnyField): any[] { + const path = []; + + while (field.parent !== null) { + path.unshift(field.key); + field = field.parent; } + return path; } function applyResult(validation: Validation, result: Err | Ok): void { if (result.ok) { return; } + + const basePath = getPath(validation.root); + for (const issue of result.issues) { - let field = validation.root; + let child = validation.root; if (issue.path !== undefined) { for (const key of issue.path) { - field = field.at(key); + child = child.at(key); } + issue.path = basePath.concat(issue.path); + } else { + issue.path = basePath.slice(0); } - setPath(validation.root, issue); - field.setValidationError(validation, issue); + child.setValidationError(validation, issue); } } diff --git a/packages/doubter-plugin/src/main/index.ts b/packages/doubter-plugin/src/main/index.ts index b23ceebd..09a034f3 100644 --- a/packages/doubter-plugin/src/main/index.ts +++ b/packages/doubter-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/doubter-plugin + * @module doubter-plugin */ export * from './doubterPlugin'; diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index ed4792ae..44de34c7 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -1,21 +1,21 @@ import { createElement, Fragment, ReactElement, ReactNode, useEffect, useReducer, useRef } from 'react'; -import { callOrGet, Field } from 'roqueform'; +import { AnyField, callOrGet, ValueOf } from 'roqueform'; /** * Properties of the {@link FieldRenderer} component. * - * @template RenderedField The rendered field. + * @template Field The rendered field. */ -export interface FieldRendererProps { +export interface FieldRendererProps { /** * The field to subscribe to. */ - field: RenderedField; + field: Field; /** * The render function that receive a field as an argument. */ - children: (field: RenderedField) => ReactNode; + children: (field: Field) => ReactNode; /** * If set to `true` then {@link FieldRenderer} is re-rendered whenever the {@link field} itself, its parent fields or @@ -31,28 +31,28 @@ export interface FieldRendererProps { * * @param value The new field value. */ - onChange?: (value: RenderedField['value']) => void; + onChange?: (value: ValueOf) => void; } /** * The component that subscribes to the {@link Field} instance and re-renders its children when the field is notified. * - * @template RenderedField The rendered field. + * @template Field The rendered field. */ -export function FieldRenderer(props: FieldRendererProps): ReactElement { +export function FieldRenderer(props: FieldRendererProps): ReactElement { const { field, eagerlyUpdated } = props; const [, rerender] = useReducer(reduceCount, 0); - const handleChangeRef = useRef['onChange']>(); + const handleChangeRef = useRef['onChange']>(); handleChangeRef.current = props.onChange; useEffect(() => { return field.on('*', event => { - if (eagerlyUpdated || event.target === field) { + if (eagerlyUpdated || event.origin === field) { rerender(); } - if (field.isTransient || event.type !== 'valueChange') { + if (field.isTransient || event.type !== 'change:value') { return; } diff --git a/packages/react/src/main/index.ts b/packages/react/src/main/index.ts index 62d5ba0a..72c786b3 100644 --- a/packages/react/src/main/index.ts +++ b/packages/react/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/react + * @module react */ export * from './AccessorContext'; diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index e2ec8e23..b2a96746 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -1,5 +1,5 @@ import { useContext, useRef } from 'react'; -import { callOrGet, createField, Field, PluginCallback } from 'roqueform'; +import { callOrGet, createField, Field, PluginInjector } from 'roqueform'; import { AccessorContext } from './AccessorContext'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types @@ -29,14 +29,14 @@ export function useField(initialValue: Value | (() => Value)): Field( initialValue: Value | (() => Value), - plugin: PluginCallback> + plugin: PluginInjector> ): Field; -export function useField(initialValue?: unknown, plugin?: PluginCallback) { +export function useField(initialValue?: unknown, plugin?: PluginInjector) { const accessor = useContext(AccessorContext); return (useRef().current ||= createField(callOrGet(initialValue, undefined), plugin!, accessor)); diff --git a/packages/ref-plugin/README.md b/packages/ref-plugin/README.md index 8eb83ead..38106775 100644 --- a/packages/ref-plugin/README.md +++ b/packages/ref-plugin/README.md @@ -26,7 +26,7 @@ export const App = () => { {nameField => ( { nameField.setValue(event.target.value); diff --git a/packages/ref-plugin/src/main/index.ts b/packages/ref-plugin/src/main/index.ts index c34a852f..681dd21f 100644 --- a/packages/ref-plugin/src/main/index.ts +++ b/packages/ref-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/ref-plugin + * @module ref-plugin */ export * from './refPlugin'; diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index d2057c68..6d5dae3a 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -1,18 +1,18 @@ -import { PluginCallback } from 'roqueform'; +import { PluginInjector } from 'roqueform'; /** * The plugin added to fields by the {@link refPlugin}. */ export interface RefPlugin { /** - * The DOM element associated with the field. + * The DOM element associated with the field, or `null` if there's no associated element. */ element: Element | null; /** * Associates the field with the {@link element DOM element}. */ - refCallback(element: Element | null): void; + ref(element: Element | null): void; /** * Scrolls the field element's ancestor containers such that the field element is visible to the user. @@ -46,15 +46,15 @@ export interface RefPlugin { /** * Enables field-element association and simplifies focus control. */ -export function refPlugin(): PluginCallback { +export function refPlugin(): PluginInjector { return field => { field.element = null; - const { refCallback } = field; + const { ref } = field; - field.refCallback = element => { + field.ref = element => { field.element = element instanceof Element ? element : null; - refCallback?.(element); + ref?.(element); }; field.scrollIntoView = options => { diff --git a/packages/ref-plugin/src/test/refPlugin.test.ts b/packages/ref-plugin/src/test/refPlugin.test.ts index 57c84d59..c55345d6 100644 --- a/packages/ref-plugin/src/test/refPlugin.test.ts +++ b/packages/ref-plugin/src/test/refPlugin.test.ts @@ -9,27 +9,27 @@ describe('refPlugin', () => { expect(field.at('bar').element).toBe(null); }); - test('refCallback updates an element property', () => { + test('ref updates an element property', () => { const field = createField({ bar: 111 }, refPlugin()); const element = document.createElement('input'); - field.refCallback(element); + field.ref(element); expect(field.element).toEqual(element); }); - test('preserves the refCallback from preceding plugin', () => { - const refCallbackMock = jest.fn(() => undefined); + test('preserves the ref from preceding plugin', () => { + const refMock = jest.fn(() => undefined); const field = createField( { bar: 111 }, - composePlugins(field => Object.assign(field, { refCallback: refCallbackMock }), refPlugin()) + composePlugins(field => Object.assign(field, { ref: refMock }), refPlugin()) ); - field.refCallback(document.body); + field.ref(document.body); - expect(refCallbackMock).toHaveBeenCalledTimes(1); - expect(refCallbackMock).toHaveBeenNthCalledWith(1, document.body); + expect(refMock).toHaveBeenCalledTimes(1); + expect(refMock).toHaveBeenNthCalledWith(1, document.body); expect(field.element).toBe(document.body); }); }); diff --git a/packages/reset-plugin/src/main/index.ts b/packages/reset-plugin/src/main/index.ts index c5c02e80..21a468e8 100644 --- a/packages/reset-plugin/src/main/index.ts +++ b/packages/reset-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/reset-plugin + * @module reset-plugin */ export * from './resetPlugin'; diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index c5c276ea..befbdb78 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,30 +1,21 @@ -import { dispatchEvents, FieldEvent, Field, isEqual, PluginCallback } from 'roqueform'; +import { dispatchEvents, Event, Field, isEqual, PluginInjector, Subscriber, Unsubscribe, ValueOf } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** * The plugin added to fields by the {@link resetPlugin}. */ export interface ResetPlugin { - /** - * @internal - */ - ['__plugin']: unknown; - - /** - * @internal - */ - value: unknown; - /** * `true` if the field value is different from its initial value, or `false` otherwise. */ - isDirty: boolean; + readonly isDirty: boolean; /** * The callback that compares initial value and the current value of the field. * * @param initialValue The initial value. * @param value The current value. + * @protected */ ['equalityChecker']: (initialValue: any, value: any) => boolean; @@ -33,7 +24,7 @@ export interface ResetPlugin { * * @param value The initial value to set. */ - setInitialValue(value: this['value']): void; + setInitialValue(value: ValueOf): void; /** * Reverts the field to its initial value. @@ -41,31 +32,13 @@ export interface ResetPlugin { reset(): void; /** - * Subscribes the listener to field initial value change events. + * Subscribes to changes of {@link FieldController.initialValue the initial value}. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. - */ - on( - eventType: 'initialValueChange', - listener: (event: InitialValueChangeEvent) => void - ): () => void; -} - -/** - * The event dispatched when the field initial value has changed. - * - * @template Plugin The plugin added to the field. - * @template Value The field value. - */ -export interface InitialValueChangeEvent extends FieldEvent { - type: 'initialValueChange'; - - /** - * The previous initial value that was replaced by {@link Field.initialValue the new initial value}. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. */ - previousInitialValue: Value; + on(eventType: 'change:initialValue', subscriber: Subscriber>): Unsubscribe; } /** @@ -76,9 +49,8 @@ export interface InitialValueChangeEvent extends */ export function resetPlugin( equalityChecker: (initialValue: any, value: any) => boolean = isDeepEqual -): PluginCallback { +): PluginInjector { return field => { - field.isDirty = field.equalityChecker(field.initialValue, field.value); field.equalityChecker = equalityChecker; field.setInitialValue = value => { @@ -89,9 +61,7 @@ export function resetPlugin( field.setValue(field.initialValue); }; - field.on('valueChange', () => { - field.isDirty = field.equalityChecker(field.initialValue, field.value); - }); + Object.defineProperty(field, 'isDirty', { get: () => field.equalityChecker(field.initialValue, field.value) }); }; } @@ -114,17 +84,11 @@ function propagateInitialValue( target: Field, field: Field, initialValue: unknown, - events: InitialValueChangeEvent[] -): InitialValueChangeEvent[] { - events.push({ - type: 'initialValueChange', - target: target as Field, - currentTarget: field as Field, - previousInitialValue: field.initialValue, - }); + events: Event[] +): Event[] { + events.push({ type: 'change:initialValue', origin: target, target: field, data: field.initialValue }); field.initialValue = initialValue; - field.isDirty = field.equalityChecker(initialValue, field.initialValue); if (field.children !== null) { for (const child of field.children) { diff --git a/packages/reset-plugin/src/test/resetPlugin.test.ts b/packages/reset-plugin/src/test/resetPlugin.test.ts index 7355d67c..4d82c26e 100644 --- a/packages/reset-plugin/src/test/resetPlugin.test.ts +++ b/packages/reset-plugin/src/test/resetPlugin.test.ts @@ -38,21 +38,21 @@ describe('resetPlugin', () => { }); test('updates the initial value and notifies fields', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const initialValue = { foo: 111 }; const field = createField(initialValue, resetPlugin()); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); const initialValue2 = { foo: 222 }; field.setInitialValue(initialValue2); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); expect(field.at('foo').initialValue).toBe(222); expect(field.at('foo').isDirty).toBe(true); diff --git a/packages/roqueform/src/main/composePlugins.ts b/packages/roqueform/src/main/composePlugins.ts index e8d287e4..4312676a 100644 --- a/packages/roqueform/src/main/composePlugins.ts +++ b/packages/roqueform/src/main/composePlugins.ts @@ -1,54 +1,62 @@ -import { PluginCallback } from './typings'; +import { PluginInjector } from './typings'; /** - * Composes a plugin from multiple plugins. + * @internal */ -export function composePlugins(a: PluginCallback, b: PluginCallback): PluginCallback
; +export function composePlugins(a: PluginInjector, b: PluginInjector): PluginInjector; /** - * Composes a plugin from multiple plugins. + * @internal */ export function composePlugins( - a: PluginCallback, - b: PluginCallback, - c: PluginCallback -): PluginCallback; + a: PluginInjector, + b: PluginInjector, + c: PluginInjector +): PluginInjector; /** - * Composes a plugin from multiple plugins. + * @internal */ export function composePlugins( - a: PluginCallback, - b: PluginCallback, - c: PluginCallback, - d: PluginCallback -): PluginCallback; + a: PluginInjector, + b: PluginInjector, + c: PluginInjector, + d: PluginInjector +): PluginInjector; /** - * Composes a plugin from multiple plugins. + * @internal */ export function composePlugins( - a: PluginCallback, - b: PluginCallback, - c: PluginCallback, - d: PluginCallback, - e: PluginCallback -): PluginCallback; + a: PluginInjector, + b: PluginInjector, + c: PluginInjector, + d: PluginInjector, + e: PluginInjector +): PluginInjector; /** - * Composes a plugin from multiple plugins. + * @internal */ export function composePlugins( - a: PluginCallback, - b: PluginCallback, - c: PluginCallback, - d: PluginCallback, - e: PluginCallback, - f: PluginCallback, - ...other: PluginCallback[] -): PluginCallback; + a: PluginInjector, + b: PluginInjector, + c: PluginInjector, + d: PluginInjector, + e: PluginInjector, + f: PluginInjector, + ...other: PluginInjector[] +): PluginInjector; -export function composePlugins(...plugins: PluginCallback[]): PluginCallback { +/** + * Composes multiple plugin callbacks into a single callback. + * + * @param plugins The array of plugins to compose. + * @returns The plugins callbacks that sequentially applies all provided plugins to a field. + */ +export function composePlugins(...plugins: PluginInjector[]): PluginInjector; + +export function composePlugins(...plugins: PluginInjector[]): PluginInjector { return field => { for (const plugin of plugins) { plugin(field); diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 8d9f6d3a..0cd98746 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,4 +1,4 @@ -import { Accessor, Field, PluginCallback, ValueChangeEvent } from './typings'; +import { Accessor, Field, Event, PluginInjector } from './typings'; import { callOrGet, dispatchEvents, isEqual } from './utils'; import { naturalAccessor } from './naturalAccessor'; @@ -28,18 +28,18 @@ export function createField(initialValue: Value, accessor?: Accessor): Fi * @param plugin The plugin that enhances the field. * @param accessor Resolves values for derived fields. * @template Value The root field initial value. - * @template Plugin The plugin added to the field. + * @template Plugin The plugin injected into the field. */ export function createField( initialValue: Value, - plugin: PluginCallback>, + plugin: PluginInjector>, accessor?: Accessor ): Field; -export function createField(initialValue?: unknown, plugin?: PluginCallback | Accessor, accessor?: Accessor) { +export function createField(initialValue?: unknown, plugin?: PluginInjector | Accessor, accessor?: Accessor) { if (typeof plugin !== 'function') { - plugin = undefined; accessor = plugin; + plugin = undefined; } return getOrCreateField(accessor || naturalAccessor, null, null, initialValue, plugin || null); } @@ -49,7 +49,7 @@ function getOrCreateField( parent: Field | null, key: unknown, initialValue: unknown, - plugin: PluginCallback | null + plugin: PluginInjector | null ): Field { let child: Field; @@ -58,7 +58,6 @@ function getOrCreateField( } child = { - __plugin: undefined, key, value: null, initialValue, @@ -67,29 +66,32 @@ function getOrCreateField( parent, children: null, childrenMap: null, - listeners: null, + subscribers: null, accessor, plugin, + setValue: value => { setValue(child, callOrGet(value, child.value), false); }, + setTransientValue: value => { setValue(child, callOrGet(value, child.value), true); }, + propagate: () => { setValue(child, child.value, false); }, - at: key => { - return getOrCreateField(child.accessor, child, key, null, plugin); - }, - on: (type, listener) => { - let listeners: unknown[]; - (listeners = (child.listeners ||= Object.create(null))[type] ||= []).push(listener); + + at: key => getOrCreateField(child.accessor, child, key, null, plugin), + + on: (type, subscriber) => { + let subscribers: unknown[]; + (subscribers = (child.subscribers ||= Object.create(null))[type] ||= []).push(subscriber); return () => { - listeners.splice(listeners.indexOf(listener), 1); + subscribers.splice(subscribers.indexOf(subscriber), 1); }; }, - }; + } satisfies Omit as unknown as Field; child.root = child; @@ -126,8 +128,8 @@ function setValue(field: Field, value: unknown, transient: boolean): void { dispatchEvents(propagateValue(field, changeRoot, value, [])); } -function propagateValue(target: Field, field: Field, value: unknown, events: ValueChangeEvent[]): ValueChangeEvent[] { - events.push({ type: 'valueChange', target, currentTarget: field, previousValue: field.value }); +function propagateValue(origin: Field, field: Field, value: unknown, events: Event[]): Event[] { + events.push({ type: 'change:value', origin, target: field, data: field.value }); field.value = value; @@ -138,10 +140,10 @@ function propagateValue(target: Field, field: Field, value: unknown, events: Val } const childValue = field.accessor.get(value, child.key); - if (child !== target && isEqual(child.value, childValue)) { + if (child !== origin && isEqual(child.value, childValue)) { continue; } - propagateValue(target, child, childValue, events); + propagateValue(origin, child, childValue, events); } } return events; diff --git a/packages/roqueform/src/main/naturalAccessor.ts b/packages/roqueform/src/main/naturalAccessor.ts index 33ed0e99..41210c79 100644 --- a/packages/roqueform/src/main/naturalAccessor.ts +++ b/packages/roqueform/src/main/naturalAccessor.ts @@ -100,7 +100,18 @@ function toArrayIndex(k: any): number { } function isPrimitive(obj: any): boolean { - return obj === null || obj === undefined || typeof obj !== 'object' || obj instanceof Date || obj instanceof RegExp; + return ( + obj === null || + obj === undefined || + typeof obj !== 'object' || + obj instanceof String || + obj instanceof Number || + obj instanceof Boolean || + obj instanceof BigInt || + obj instanceof Symbol || + obj instanceof Date || + obj instanceof RegExp + ); } function isMapLike(obj: any): obj is Map { diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 94900dff..11a062a1 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -1,29 +1,102 @@ /** - * The field describes field that holds a value and provides means to update it. Fields can be enhanced by plugins that + * The field that doesn't constrain its children and ancestors. Use this in plugins to streamline typing. + * + * @template Plugin The plugin injected into the field. + */ +export type AnyField = FieldController & Plugin; + +/** + * The field that manages a value and related data. Fields can be {@link PluginInjector enhanced by plugins} that * provide integration with rendering frameworks, validation libraries, and other tools. * - * @template Plugin The plugin added to the field. + * @template Plugin The plugin injected into the field. * @template Value The field value. */ export type Field = FieldController & Plugin; /** - * The baseline field controller that can be enhanced by plugins. + * The event dispatched to subscribers of {@link Field a field}. * - * @template Plugin The plugin added to the field. + * @template Target The field where the event is dispatched. + * @template Data The additional data related to the event. + */ +export interface Event { + /** + * The type of the event. + */ + type: string; + + /** + * The field that has changed. + */ + target: Target; + + /** + * The original field that caused the event to be dispatched. This can be ancestor, descendant, or the{@link target} + * itself. + */ + origin: Field>; + + /** + * The additional data related to the event, depends on the {@link type event type}. + */ + data: Data; +} + +/** + * The callback that receives events dispatched by {@link Field a field}. + * + * @param event The dispatched event. + * @template Target The field where the event is dispatched. + * @template Data The additional data related to the event. + */ +export type Subscriber = (event: Event) => void; + +/** + * Unsubscribes the subscriber. No-op if subscriber was already unsubscribed. + */ +export type Unsubscribe = () => void; + +/** + * Infers the plugin that was used to enhance the field. + * + * Use `PluginOf` in plugin interfaces to infer all plugin interfaces that were intersected with the field + * controller. + * + * @template T The field to infer plugin of. + */ +export type PluginOf = T['__plugin__' & keyof T]; + +/** + * Infers the value of the field. + * + * Use `ValueOf` in plugin interfaces to infer the value of the current field. + * + * @template T The field to infer value of. + */ +export type ValueOf = T['value' & keyof T]; + +/** + * The field controller provides the core field functionality. + * + * @template Plugin The plugin injected into the field. * @template Value The field value. */ -interface FieldController { +export interface FieldController { /** + * Holds the plugin type for inference. + * + * Use {@link PluginOf PluginOf} in plugin interfaces to infer the plugin type. + * * @internal */ - ['__plugin']: Plugin; + readonly ['__plugin__']: Plugin; /** * The key in the {@link parent parent value} that corresponds to the value of this field, or `null` if there's no * parent. */ - readonly key: any; + key: any; /** * The current value of the field. @@ -37,54 +110,76 @@ interface FieldController { /** * `true` if the value was last updated using {@link setTransientValue}, or `false` otherwise. + * + * @see [Transient updates](https://github.com/smikhalevski/roqueform#transient-updates) */ isTransient: boolean; /** * The root field. + * + * @protected */ ['root']: Field; /** * The parent field, or `null` if this is the root field. + * + * @protected */ - readonly ['parent']: Field | null; + ['parent']: Field | null; /** - * The array of immediate child fields that were {@link at previously accessed}, or `null` if there's no children. - * Children array is always in sync with {@link childrenMap}. + * The array of immediate child fields that were {@link at previously accessed}, or `null` if there are no children. + * + * This array is populated during {@link FieldController.at} call. * - * Don't modify this array directly and always use on {@link at} to add a new child. + * @see {@link childrenMap} + * @protected */ ['children']: Field[] | null; /** - * Mapping from a key to a child field. Children map is always in sync with {@link children children array}. + * Mapping from a key to a corresponding child field, or `null` if there are no children. + * + * This map is populated during {@link FieldController.at} call. * - * Don't modify this array directly and always use on {@link at} to add a new child. + * @see {@link children} + * @protected */ ['childrenMap']: Map> | null; /** - * The map from an event type to an array of associated listeners, or `null` if no listeners were added. + * The map from an event type to an array of associated subscribers, or `null` if no subscribers were added. + * + * @see {@link on} + * @protected */ - ['listeners']: { [eventType: string]: Array<(event: FieldEvent) => void> } | null; + ['subscribers']: Record[]> | null; /** * The accessor that reads the field value from the value of the parent fields, and updates parent value. + * + * @see [Accessors](https://github.com/smikhalevski/roqueform#accessors) + * @protected */ - readonly ['accessor']: Accessor; + ['accessor']: Accessor; /** - * The plugin that is applied to this field and all child field when they are accessed. + * The plugin that is applied to this field and all child fields when they are accessed, or `null` field isn't + * enhanced by a plugin. + * + * @see [Authoring a plugin](https://github.com/smikhalevski/roqueform#authoring-a-plugin) + * @protected */ - readonly ['plugin']: PluginCallback | null; + ['plugin']: PluginInjector | null; /** - * Updates the field value and notifies both ancestor and child fields about the change. If the field withholds + * Updates the field value and notifies both ancestors and child fields about the change. If the field withholds * {@link isTransient a transient value} then it becomes non-transient. * * @param value The value to set, or a callback that receives a previous value and returns a new one. + * @see [Transient updates](https://github.com/smikhalevski/roqueform#transient-updates) */ setValue(value: Value | ((prevValue: Value) => Value)): void; @@ -93,12 +188,13 @@ interface FieldController { * {@link isTransient transient}. * * @param value The value to set, or a callback that receives a previous value and returns a new one. + * @see [Transient updates](https://github.com/smikhalevski/roqueform#transient-updates) */ setTransientValue(value: Value | ((prevValue: Value) => Value)): void; /** * If {@link value the current value} {@link isTransient is transient} then the value of the parent field is notified - * about the change and this field is marked as non-transient. + * about the change and this field is marked as non-transient. No-op if the current value is non-transient. */ propagate(): void; @@ -113,71 +209,32 @@ interface FieldController { at>(key: Key): Field>; /** - * Subscribes the listener to all events. + * Subscribes to all events. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. */ - on(eventType: '*', listener: (event: FieldEvent) => void): () => void; + on(eventType: '*', subscriber: Subscriber): Unsubscribe; /** - * Subscribes the listener to field value change events. + * Subscribes to {@link value the field value} changes. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. - */ - on(eventType: 'valueChange', listener: (event: ValueChangeEvent) => void): () => void; -} - -/** - * The event dispatched to subscribers of {@link Field a field}. - * - * @template Plugin The plugin added to the field. - * @template Value The field value. - */ -export interface FieldEvent { - /** - * The type of the event. - */ - type: string; - - /** - * The field that caused the event to be dispatched. This can be ancestor, descendant, or the {@link currentTarget}. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. */ - target: Field; - - /** - * The field to which the event listener is subscribed. - */ - currentTarget: Field; + on(eventType: 'change:value', subscriber: Subscriber>): Unsubscribe; } /** - * The event dispatched when the field value has changed. - * - * @template Plugin The plugin added to the field. - * @template Value The field value. - */ -export interface ValueChangeEvent extends FieldEvent { - type: 'valueChange'; - - /** - * The previous value that was replaced by {@link Field.value the current field value}. - */ - previousValue: Value; -} - -/** - * The callback that enhances the field. - * - * The plugin should _mutate_ the passed field instance. + * The callback that enhances the field with a plugin. Injector should _mutate_ the passed field instance. * - * @template Plugin The plugin added to the field. + * @param field The mutable field that must be enhanced. + * @template Plugin The plugin injected into the field. * @template Value The root field value. */ -export type PluginCallback = (field: Field) => void; +export type PluginInjector = (field: Field) => void; /** * The abstraction used by the {@link Field} to read and write object properties. diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 6ec67fd4..1a29ed1c 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,4 +1,4 @@ -import { FieldEvent } from './typings'; +import { Event } from './typings'; export function callOrGet(value: T | ((prevValue: A) => T), prevValue: A): T { return typeof value === 'function' ? (value as Function)(prevValue) : value; @@ -15,22 +15,22 @@ export function isEqual(a: unknown, b: unknown): boolean { return a === b || (a !== a && b !== b); } -export function dispatchEvents>(events: readonly E[]): void { +export function dispatchEvents(events: readonly Event[]): void { for (const event of events) { - const { listeners } = event.currentTarget; + const { subscribers } = event.target; - if (listeners !== null) { - callAll(listeners[event.type], event); - callAll(listeners['*'], event); + if (subscribers !== null) { + callAll(subscribers[event.type], event); + callAll(subscribers['*'], event); } } } -function callAll(listeners: Array<(event: FieldEvent) => void> | undefined, event: FieldEvent): void { - if (listeners !== undefined) { - for (const listener of listeners) { +function callAll(subscribers: Array<(event: Event) => void> | undefined, event: Event): void { + if (subscribers !== undefined) { + for (const subscriber of subscribers) { try { - listener(event); + subscriber(event); } catch (error) { setTimeout(() => { throw error; diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 304a33f0..cbe3efab 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,25 +1,25 @@ -import { FieldEvent, Field, PluginCallback } from './typings'; +import { Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from './typings'; import { dispatchEvents, isEqual } from './utils'; -const EVENT_CHANGE = 'validityChange'; -const EVENT_START = 'validationStart'; -const EVENT_END = 'validationEnd'; +const EVENT_CHANGE_ERROR = 'change:error'; const ERROR_ABORT = 'Validation aborted'; /** - * Describes the pending validation. + * The pending validation descriptor. + * + * @template Plugin The plugin injected into the field. */ export interface Validation { /** * The field where the validation was triggered. */ - readonly root: Field; + root: Field; /** * The abort controller associated with the pending {@link Validator.validateAsync async validation}, or `null` if * the validation is synchronous. */ - readonly abortController: AbortController | null; + abortController: AbortController | null; } /** @@ -29,23 +29,13 @@ export interface Validation { * @template Options Options passed to the validator. */ export interface ValidationPlugin { - /** - * @internal - */ - ['__plugin']: unknown; - - /** - * @internal - */ - value: unknown; - /** * A validation error associated with the field, or `null` if there's no error. */ error: Error | null; /** - * `true` if this field or any of its descendants have no associated errors, or `false` otherwise. + * `true` if this field or any of its descendants have associated errors, or `false` otherwise. */ readonly isInvalid: boolean; @@ -56,6 +46,8 @@ export interface ValidationPlugin { /** * The total number of errors associated with this field and its child fields. + * + * @protected */ ['errorCount']: number; @@ -64,18 +56,24 @@ export interface ValidationPlugin { * - 0 if there's no associated error. * - 1 if an error was set using {@link ValidationPlugin.setValidationError}; * - 2 if an error was set using {@link ValidationPlugin.setError}; + * + * @protected */ ['errorOrigin']: 0 | 1 | 2; /** * The validator to which the field value validation is delegated. + * + * @protected */ ['validator']: Validator; /** * The pending validation, or `null` if there's no pending validation. + * + * @protected */ - ['validation']: Validation | null; + ['validation']: Validation> | null; /** * Associates an error with the field and notifies the subscribers. @@ -102,14 +100,14 @@ export interface ValidationPlugin { /** * Returns all fields that have an error. */ - getInvalidFields(): Field[]; + getInvalidFields(): Field>[]; /** * Triggers a sync field validation. Starting a validation will clear errors that were set during the previous * validation and preserve errors set via {@link setError}. If you want to clear all errors before the validation, * use {@link clearErrors}. * - * @param options Options passed to the validator. + * @param options Options passed to {@link Validator the validator}. * @returns `true` if {@link isInvalid field is valid}, or `false` otherwise. */ validate(options?: Options): boolean; @@ -119,7 +117,7 @@ export interface ValidationPlugin { * validation and preserve errors set via {@link setError}. If you want to clear all errors before the validation, * use {@link clearErrors}. * - * @param options Options passed to the validator. + * @param options Options passed to {@link Validator the validator}. * @returns `true` if {@link isInvalid field is valid}, or `false` otherwise. */ validateAsync(options?: Options): Promise; @@ -131,44 +129,51 @@ export interface ValidationPlugin { abortValidation(): void; /** - * Subscribes the listener field validity change events. An {@link error} would contain the associated error. + * Subscribes to {@link error an associated error} changes of this field or any of its descendants. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. + * @see {@link error} + * @see {@link isInvalid} */ - on(eventType: 'validityChange', listener: (event: FieldEvent) => void): () => void; + on(eventType: 'change:error', subscriber: Subscriber): Unsubscribe; /** - * Subscribes the listener to validation start events. The event is triggered for all fields that are going to be - * validated. The {@link FieldController.value current value} of the field is the one that is being validated. - * {@link FieldEvent.target} points to the field where validation was triggered. + * Subscribes to the start of the validation. The event is triggered for all fields that are going to be validated. + * The {@link FieldController.value current value} of the field is the one that is being validated. + * {@link Event.origin} points to the field where validation was triggered. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. + * @see {@link validation} + * @see {@link isValidating} */ - on(eventType: 'validationStart', listener: (event: FieldEvent) => void): () => void; + on(eventType: 'validation:start', subscriber: Subscriber): Unsubscribe; /** - * Subscribes the listener to validation start end events. The event is triggered for all fields that were validated. - * {@link FieldEvent.target} points to the field where validation was triggered. Check {@link isInvalid} to detect the + * Subscribes to the end of the validation. The event is triggered for all fields that were validated when validation. + * {@link Event.origin} points to the field where validation was triggered. Check {@link isInvalid} to detect the * actual validity status. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. + * @see {@link validation} + * @see {@link isValidating} */ - on(eventType: 'validationEnd', listener: (event: FieldEvent) => void): () => void; + on(eventType: 'validation:end', subscriber: Subscriber): Unsubscribe; /** * Associates a validation error with the field and notifies the subscribers. Use this method in * {@link Validator validators} to set errors that can be overridden during the next validation. * - * @param validation The validation in scope of which the value is set. + * @param validation The validation in scope of which an error must be set. * @param error The error to set. + * @protected */ - ['setValidationError'](validation: Validation, error: Error): void; + ['setValidationError'](validation: Validation>, error: Error): void; } /** @@ -218,7 +223,7 @@ export interface Validator { */ export function validationPlugin( validator: Validator | Validator['validate'] -): PluginCallback> { +): PluginInjector> { return field => { field.error = null; field.errorCount = 0; @@ -281,30 +286,23 @@ export function validationPlugin( }; } -type ValidationEvent = FieldEvent; - -function setError( - field: Field, - error: unknown, - errorOrigin: 1 | 2, - events: ValidationEvent[] -): ValidationEvent[] { +function setError(field: Field, error: unknown, errorOrigin: 1 | 2, events: Event[]): Event[] { if (error === null || error === undefined) { return deleteError(field, errorOrigin, events); } - const errored = field.error !== null; + const originalError = field.error; - if (errored && isEqual(field.error, error) && field.errorOrigin === errorOrigin) { + if (originalError !== null && isEqual(originalError, error) && field.errorOrigin === errorOrigin) { return events; } field.error = error; field.errorOrigin = errorOrigin; - events.push({ type: EVENT_CHANGE, target: field, currentTarget: field }); + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); - if (errored) { + if (originalError !== null) { return events; } @@ -312,15 +310,17 @@ function setError( for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { if (ancestor.errorCount++ === 0) { - events.push({ type: EVENT_CHANGE, target: field, currentTarget: ancestor }); + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); } } return events; } -function deleteError(field: Field, errorOrigin: 1 | 2, events: ValidationEvent[]): ValidationEvent[] { - if (field.error === null || field.errorOrigin > errorOrigin) { +function deleteError(field: Field, errorOrigin: 1 | 2, events: Event[]): Event[] { + const originalError = field.error; + + if (originalError === null || field.errorOrigin > errorOrigin) { return events; } @@ -328,18 +328,18 @@ function deleteError(field: Field, errorOrigin: 1 | 2, events: field.errorOrigin = 0; field.errorCount--; - events.push({ type: EVENT_CHANGE, target: field, currentTarget: field }); + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { if (--ancestor.errorCount === 0) { - events.push({ type: EVENT_CHANGE, target: field, currentTarget: ancestor }); + events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); } } return events; } -function clearErrors(field: Field, errorOrigin: 1 | 2, events: ValidationEvent[]): ValidationEvent[] { +function clearErrors(field: Field, errorOrigin: 1 | 2, events: Event[]): Event[] { deleteError(field, errorOrigin, events); if (field.children !== null) { @@ -353,14 +353,10 @@ function clearErrors(field: Field, errorOrigin: 1 | 2, events: return events; } -function startValidation( - field: Field, - validation: Validation, - events: ValidationEvent[] -): ValidationEvent[] { +function startValidation(field: Field, validation: Validation, events: Event[]): Event[] { field.validation = validation; - events.push({ type: EVENT_START, target: validation.root, currentTarget: field }); + events.push({ type: 'validation:start', origin: validation.root, target: field, data: undefined }); if (field.children !== null) { for (const child of field.children) { @@ -372,18 +368,14 @@ function startValidation( return events; } -function endValidation( - field: Field, - validation: Validation, - events: ValidationEvent[] -): ValidationEvent[] { +function endValidation(field: Field, validation: Validation, events: Event[]): Event[] { if (field.validation !== validation) { return events; } field.validation = null; - events.push({ type: EVENT_END, target: validation.root, currentTarget: field }); + events.push({ type: 'validation:end', origin: validation.root, target: field, data: undefined }); if (field.children !== null) { for (const child of field.children) { @@ -394,7 +386,7 @@ function endValidation( return events; } -function abortValidation(field: Field, events: ValidationEvent[]): ValidationEvent[] { +function abortValidation(field: Field, events: Event[]): Event[] { const { validation } = field; if (validation !== null) { @@ -404,6 +396,25 @@ function abortValidation(field: Field, events: ValidationEvent return events; } +function getInvalidFields(field: Field, batch: Field[]): Field[] { + if (field.error !== null) { + batch.push(field); + } + if (field.children !== null) { + for (const child of field.children) { + getInvalidFields(child, batch); + } + } + return batch; +} + +function convertToErrors(batch: Field[]): any[] { + for (let i = 0; i < batch.length; ++i) { + batch[i] = batch[i].error; + } + return batch; +} + function validate(field: Field, options: unknown): boolean { dispatchEvents(clearErrors(field, 1, abortValidation(field, []))); @@ -473,22 +484,3 @@ function validateAsync(field: Field, options: unknown): Promis } ); } - -function getInvalidFields(field: Field, batch: Field[]): Field[] { - if (field.error !== null) { - batch.push(field.error); - } - if (field.children !== null) { - for (const child of field.children) { - getInvalidFields(child, batch); - } - } - return batch; -} - -function convertToErrors(batch: Field[]): any[] { - for (let i = 0; i < batch.length; ++i) { - batch[i] = batch[i].error; - } - return batch; -} diff --git a/packages/roqueform/src/test/createField.test.ts b/packages/roqueform/src/test/createField.test.ts index 5955d216..0c65157d 100644 --- a/packages/roqueform/src/test/createField.test.ts +++ b/packages/roqueform/src/test/createField.test.ts @@ -58,18 +58,18 @@ describe('createField', () => { }); test('invokes a subscriber when value is updated', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const field = createField({ foo: 111 }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setValue(222); - expect(listenerMock).toHaveBeenCalledTimes(1); - expect(listenerMock).toHaveBeenNthCalledWith(1, field.at('foo'), field); + expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenNthCalledWith(1, field.at('foo'), field); expect(fooListenerMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenNthCalledWith(1, field.at('foo'), field.at('foo')); @@ -146,22 +146,22 @@ describe('createField', () => { }); test('invokes a subscriber when a value is updated transiently', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const field = createField({ foo: 111 }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setTransientValue(222); - expect(listenerMock).toHaveBeenCalledTimes(0); + expect(subscriberMock).toHaveBeenCalledTimes(0); expect(fooListenerMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenNthCalledWith(1, field.at('foo'), field.at('foo')); }); - test('does not leave the form in an inconsistent state if a listener throws an error', () => { + test('does not leave the form in an inconsistent state if a subscriber throws an error', () => { const fooListenerMock = jest.fn(() => { throw new Error('fooExpected'); }); @@ -185,23 +185,23 @@ describe('createField', () => { expect(() => jest.runAllTimers()).toThrow(new Error('barExpected')); }); - test('calls all listeners and throws error asynchronously', () => { - const listenerMock1 = jest.fn(() => { + test('calls all subscribers and throws error asynchronously', () => { + const subscriberMock1 = jest.fn(() => { throw new Error('expected1'); }); - const listenerMock2 = jest.fn(() => { + const subscriberMock2 = jest.fn(() => { throw new Error('expected2'); }); const field = createField({ foo: 111, bar: 222 }); - field.at('foo').subscribe(listenerMock1); - field.at('foo').subscribe(listenerMock2); + field.at('foo').subscribe(subscriberMock1); + field.at('foo').subscribe(subscriberMock2); field.setValue({ foo: 333, bar: 444 }); - expect(listenerMock1).toHaveBeenCalledTimes(1); - expect(listenerMock2).toHaveBeenCalledTimes(1); + expect(subscriberMock1).toHaveBeenCalledTimes(1); + expect(subscriberMock2).toHaveBeenCalledTimes(1); expect(field.at('foo').value).toBe(333); expect(field.at('bar').value).toBe(444); @@ -210,18 +210,18 @@ describe('createField', () => { }); test('propagates a new value to the derived field', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const field = createField({ foo: 111 }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); const nextValue = { foo: 333 }; field.setValue(nextValue); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); expect(field.value).toBe(nextValue); @@ -232,18 +232,18 @@ describe('createField', () => { }); test('does not propagate a new value to the transient derived field', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const field = createField({ foo: 111 }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setTransientValue(222); field.setValue({ foo: 333 }); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); expect(field.at('foo').value).toBe(222); @@ -251,20 +251,20 @@ describe('createField', () => { }); test('does not notify subscribers if a value of the derived field did not change', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const fooValue = { bar: 111 }; const field = createField({ foo: fooValue }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.setValue({ foo: fooValue }); - expect(listenerMock).toHaveBeenCalledTimes(1); - expect(listenerMock).toHaveBeenNthCalledWith(1, field, field); + expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenNthCalledWith(1, field, field); expect(fooListenerMock).toHaveBeenCalledTimes(0); }); @@ -307,21 +307,21 @@ describe('createField', () => { const plugin: Plugin = jest.fn().mockImplementationOnce((_field, _accessor, notify) => { notifyCallback1 = notify; }); - const listenerMock1 = jest.fn(); - const listenerMock2 = jest.fn(); + const subscriberMock1 = jest.fn(); + const subscriberMock2 = jest.fn(); const field = createField({ foo: 111 }, plugin); - field.subscribe(listenerMock1); - field.at('foo').subscribe(listenerMock2); + field.subscribe(subscriberMock1); + field.at('foo').subscribe(subscriberMock2); - expect(listenerMock1).not.toHaveBeenCalled(); - expect(listenerMock2).not.toHaveBeenCalled(); + expect(subscriberMock1).not.toHaveBeenCalled(); + expect(subscriberMock2).not.toHaveBeenCalled(); notifyCallback1(); - expect(listenerMock1).toHaveBeenCalledTimes(1); - expect(listenerMock2).not.toHaveBeenCalled(); + expect(subscriberMock1).toHaveBeenCalledTimes(1); + expect(subscriberMock2).not.toHaveBeenCalled(); }); test('plugin notifies derived field subscribers', () => { @@ -334,24 +334,24 @@ describe('createField', () => { notifyCallback1 = notify; }); - const listenerMock1 = jest.fn(); - const listenerMock2 = jest.fn(); + const subscriberMock1 = jest.fn(); + const subscriberMock2 = jest.fn(); const field = createField({ foo: 111 }, plugin); - field.subscribe(listenerMock1); - field.at('foo').subscribe(listenerMock2); + field.subscribe(subscriberMock1); + field.at('foo').subscribe(subscriberMock2); - expect(listenerMock1).not.toHaveBeenCalled(); - expect(listenerMock2).not.toHaveBeenCalled(); + expect(subscriberMock1).not.toHaveBeenCalled(); + expect(subscriberMock2).not.toHaveBeenCalled(); notifyCallback1(); - expect(listenerMock1).not.toHaveBeenCalled(); - expect(listenerMock2).toHaveBeenCalledTimes(1); + expect(subscriberMock1).not.toHaveBeenCalled(); + expect(subscriberMock2).toHaveBeenCalledTimes(1); }); - test('an actual parent value is visible in the derived field listener', done => { + test('an actual parent value is visible in the derived field subscriber', done => { const field = createField({ foo: 111 }); const newValue = { foo: 222 }; @@ -380,33 +380,33 @@ describe('createField', () => { expect(() => field.at('foo')).toThrow(new Error('expected2')); }); - test('setting field value in a listener does not trigger an infinite loop', () => { + test('setting field value in a subscriber does not trigger an infinite loop', () => { const field = createField(111); - const listenerMock = jest.fn(() => { + const subscriberMock = jest.fn(() => { field.setValue(333); }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.setValue(222); expect(field.value).toBe(333); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); }); - test('setting field value in a derived field listener does not trigger an infinite loop', () => { + test('setting field value in a derived field subscriber does not trigger an infinite loop', () => { const field = createField({ foo: 111 }); - const listenerMock = jest.fn(() => { + const subscriberMock = jest.fn(() => { field.at('foo').setValue(333); }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').setValue(222); expect(field.value.foo).toBe(333); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/roqueform/src/test/validationPlugin.test.tsx b/packages/roqueform/src/test/validationPlugin.test.tsx index dd667892..70baae65 100644 --- a/packages/roqueform/src/test/validationPlugin.test.tsx +++ b/packages/roqueform/src/test/validationPlugin.test.tsx @@ -20,10 +20,10 @@ describe('validationPlugin', () => { test('sets an error to the root field', () => { const field = createField({ foo: 0 }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.setError(111); @@ -34,17 +34,17 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(false); expect(field.at('foo').error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).not.toHaveBeenCalled(); }); test('sets an error to the child field', () => { const field = createField({ foo: 0 }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setError(111); @@ -55,7 +55,7 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe(111); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); @@ -74,10 +74,10 @@ describe('validationPlugin', () => { test('deletes an error from the root field', () => { const field = createField({ foo: 0 }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.setError(111); @@ -89,17 +89,17 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(false); expect(field.at('foo').error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(fooListenerMock).not.toHaveBeenCalled(); }); test('deletes an error from the child field', () => { const field = createField({ foo: 0 }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').setError(111); @@ -111,18 +111,18 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(false); expect(field.at('foo').error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(fooListenerMock).toHaveBeenCalledTimes(2); }); test('deletes an error from the child field but parent remains invalid', () => { const field = createField({ foo: 0, bar: 'qux' }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const barListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('bar').subscribe(barListenerMock); @@ -140,7 +140,7 @@ describe('validationPlugin', () => { expect(field.at('bar').isInvalid).toBe(false); expect(field.at('bar').error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); expect(barListenerMock).toHaveBeenCalledTimes(2); }); @@ -148,11 +148,11 @@ describe('validationPlugin', () => { test('clears all errors', () => { const field = createField({ foo: 0, bar: 'qux' }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); const barListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('bar').subscribe(barListenerMock); @@ -170,7 +170,7 @@ describe('validationPlugin', () => { expect(field.at('bar').isInvalid).toBe(false); expect(field.at('bar').error).toBe(null); - expect(listenerMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(fooListenerMock).toHaveBeenCalledTimes(2); expect(barListenerMock).toHaveBeenCalledTimes(2); }); @@ -212,10 +212,10 @@ describe('validationPlugin', () => { }) ); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); expect(field.validate()).toEqual([111]); @@ -228,7 +228,7 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe(111); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); @@ -240,10 +240,10 @@ describe('validationPlugin', () => { }) ); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); expect(field.validate()).toEqual([111]); @@ -256,7 +256,7 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe(111); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); @@ -270,10 +270,10 @@ describe('validationPlugin', () => { }) ); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); field.at('foo').validate(); @@ -286,7 +286,7 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe(111); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(1); }); @@ -408,10 +408,10 @@ describe('validationPlugin', () => { }) ); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); const promise = field.validateAsync(); @@ -431,7 +431,7 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe(111); - expect(listenerMock).toHaveBeenCalledTimes(3); + expect(subscriberMock).toHaveBeenCalledTimes(3); expect(fooListenerMock).toHaveBeenCalledTimes(3); }); @@ -447,10 +447,10 @@ describe('validationPlugin', () => { }) ); - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const fooListenerMock = jest.fn(); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').subscribe(fooListenerMock); const promise = field.at('foo').validateAsync(); @@ -468,7 +468,7 @@ describe('validationPlugin', () => { expect(field.at('foo').isInvalid).toBe(true); expect(field.at('foo').error).toBe(111); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(fooListenerMock).toHaveBeenCalledTimes(3); }); @@ -627,14 +627,14 @@ describe('validationPlugin', () => { test('validation can be called in subscribe', () => { const field = createField({ foo: 0 }, validationPlugin(noopValidator)); - const listenerMock = jest.fn(() => { + const subscriberMock = jest.fn(() => { field.validate(); }); - field.subscribe(listenerMock); + field.subscribe(subscriberMock); field.at('foo').setValue(111); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/scroll-to-error-plugin/README.md b/packages/scroll-to-error-plugin/README.md index f0c886ac..b6825135 100644 --- a/packages/scroll-to-error-plugin/README.md +++ b/packages/scroll-to-error-plugin/README.md @@ -60,7 +60,7 @@ export const App = () => { <> { nameField.setValue(event.target.value); diff --git a/packages/scroll-to-error-plugin/src/main/index.ts b/packages/scroll-to-error-plugin/src/main/index.ts index 607410d5..850f6f5d 100644 --- a/packages/scroll-to-error-plugin/src/main/index.ts +++ b/packages/scroll-to-error-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/scroll-to-error-plugin + * @module scroll-to-error-plugin */ export * from './scrollToErrorPlugin'; diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 927f2c5f..222c8bda 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -1,4 +1,4 @@ -import { Field, PluginCallback } from 'roqueform'; +import { Field, PluginInjector } from 'roqueform'; export interface ScrollToErrorOptions extends ScrollIntoViewOptions { /** @@ -12,19 +12,19 @@ export interface ScrollToErrorOptions extends ScrollIntoViewOptions { */ export interface ScrollToErrorPlugin { /** - * @internal + * An error associated with the field, or `null` if there's no error. */ error: unknown; /** - * The DOM element associated with the field. + * The DOM element associated with the field, or `null` if there's no associated element. */ element: Element | null; /** - * Associates the field with the DOM element. + * Associates the field with the {@link element DOM element}. */ - refCallback(element: Element | null): void; + ref(element: Element | null): void; /** * Scroll to the element that is referenced by a field that has an associated error. Scrolls the field element's @@ -63,13 +63,16 @@ export interface ScrollToErrorPlugin { * Use this plugin in conjunction with another plugin that adds validation methods and manages `error` property of each * field. */ -export function scrollToErrorPlugin(): PluginCallback { +export function scrollToErrorPlugin(): PluginInjector { return field => { - const { refCallback } = field; + field.error = null; + field.element = null; - field.refCallback = element => { + const { ref } = field; + + field.ref = element => { field.element = element; - refCallback?.(element); + ref?.(element); }; field.scrollToError = (index = 0, options) => { diff --git a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx index 9680eba6..a5e801c0 100644 --- a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx +++ b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx @@ -48,8 +48,8 @@ describe('scrollToErrorPlugin', () => { const fooElement = document.body.appendChild(document.createElement('input')); const barElement = document.body.appendChild(document.createElement('input')); - rootField.at('foo').refCallback(fooElement); - rootField.at('bar').refCallback(barElement); + rootField.at('foo').ref(fooElement); + rootField.at('bar').ref(barElement); jest.spyOn(fooElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(100)); jest.spyOn(barElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(200)); @@ -124,8 +124,8 @@ describe('scrollToErrorPlugin', () => { const fooElement = document.body.appendChild(document.createElement('input')); const barElement = document.body.appendChild(document.createElement('input')); - rootField.at('foo').refCallback(fooElement); - rootField.at('bar').refCallback(barElement); + rootField.at('foo').ref(fooElement); + rootField.at('bar').ref(barElement); jest.spyOn(fooElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(100)); jest.spyOn(barElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(200)); diff --git a/packages/uncontrolled-plugin/README.md b/packages/uncontrolled-plugin/README.md index 5596ea19..152519e6 100644 --- a/packages/uncontrolled-plugin/README.md +++ b/packages/uncontrolled-plugin/README.md @@ -36,7 +36,7 @@ export const App = () => { {'Planet:'}
@@ -47,7 +47,7 @@ export const App = () => { type="radio" name="color-property" value={color} - ref={field.at('properties').at('color').refCallback} + ref={field.at('properties').at('color').ref} /> {color} @@ -59,17 +59,17 @@ export const App = () => { # Value coercion To associate field with a form element, pass -[`Field.refCallback`](https://smikhalevski.github.io/roqueform/interfaces/_roqueform_ref_plugin.RefPlugin.html#refCallback) +[`Field.ref`](https://smikhalevski.github.io/roqueform/interfaces/_roqueform_ref_plugin.RefPlugin.html#ref) as a `ref` attribute of an `input`, `textarea`, or any other form element: ```tsx - + ``` The plugin would synchronize the field value with the value of an input element. When the input value is changed and `change` or `input` event is dispatched, `field` is updated with the corresponding value. -If you have a set of radio buttons, or checkboxes that update a single field, provide the same `refCallback` to all +If you have a set of radio buttons, or checkboxes that update a single field, provide the same `ref` to all inputs, `uncontrolledPlugin` would use them a source of values. ```ts @@ -83,13 +83,13 @@ The plugin relies only on `value` attribute, so `name` and other attributes are ``` diff --git a/packages/uncontrolled-plugin/src/main/index.ts b/packages/uncontrolled-plugin/src/main/index.ts index 65f00a83..081639ed 100644 --- a/packages/uncontrolled-plugin/src/main/index.ts +++ b/packages/uncontrolled-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/uncontrolled-plugin + * @module uncontrolled-plugin */ export * from './createElementValueAccessor'; diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index b880ddd7..c5e735ea 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -1,8 +1,8 @@ -import { dispatchEvents, Field, FieldEvent, PluginCallback } from 'roqueform'; +import { dispatchEvents, Event as Event_, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; -const EVENT_TRACK = 'trackedElementsChange'; +const EVENT_CHANGE_OBSERVED_ELEMENTS = 'change:observedElements'; /** * The default value accessor. @@ -14,104 +14,89 @@ const elementValueAccessor = createElementValueAccessor(); */ export interface UncontrolledPlugin { /** - * @internal - */ - ['__plugin']: unknown; - - /** - * @internal - */ - ['value']: unknown; - - /** - * The array of that are used to derive the field value. Update this array by calling {@link track} method. Elements - * are watched by {@link MutationObserver} and removed from this array when they are removed from the DOM. + * The array of elements that are used to derive the field value. Update this array by calling {@link observe} method. + * Elements are observed by the {@link MutationObserver} and deleted from this array when they are removed from DOM. + * + * @protected */ - ['trackedElements']: Element[]; + ['observedElements']: Element[]; /** - * The accessor that reads and writes field value from and to {@link trackedElements tracked elements}. + * The accessor that reads and writes field value from and to {@link observedElements observed elements}. + * + * @protected */ ['elementValueAccessor']: ElementValueAccessor; /** - * The adds the DOM element to the list of tracked elements. + * The adds the DOM element to {@link observedElements observed elements}. * - * @param element The element to track. No-op if the element is `null` or not connected to the DOM. + * @param element The element to observe. No-op if the element is `null` or not connected to the DOM. */ - track(element: Element | null): void; + observe(element: Element | null): void; /** - * Overrides {@link elementValueAccessor the element value accessor} for the field. + * Overrides {@link elementValueAccessor the element value accessor} for this field. * * @param accessor The accessor to use for this filed. */ - setElementValueAccessor(accessor: Partial): this; + setElementValueAccessor(accessor: ElementValueAccessor): this; /** - * Subscribes the listener to additions and deletions in of {@link trackedElements tracked elements}. + * Subscribes to updates of {@link observedElements observed elements}. * * @param eventType The type of the event. - * @param listener The listener that would be triggered. - * @returns The callback to unsubscribe the listener. + * @param subscriber The subscriber that would be triggered. + * @returns The callback to unsubscribe the subscriber. */ - on( - eventType: 'trackedElementsChange', - listener: (event: FieldEvent) => void - ): () => void; + on(eventType: 'change:observedElements', subscriber: Subscriber): Unsubscribe; /** * Associates the field with {@link element the DOM element}. This method is usually exposed by plugins that use DOM - * element references. This method is invoked when {@link trackedElements the first tracked element} is changed. - */ - ['refCallback']?(element: Element | null): void; -} - -export interface TrackedElementsChangeEvent extends FieldEvent { - type: 'trackedElementsChange'; - - /** - * The element that was added or removed from {@link trackedElements tracked elements}. + * element references. This method is invoked when {@link observedElements the first observed element} is changed. + * + * @protected */ - element: Element; + ['ref']?(element: Element | null): void; } /** * Updates field value when the DOM element value is changed and vice versa. * - * @param defaultAccessor The accessor that reads and writes value to and from the DOM elements managed by the filed. + * @param accessor The accessor that reads and writes values to and from the DOM elements that + * {@link UncontrolledPlugin.observedElements are observed by the filed}. */ -export function uncontrolledPlugin(defaultAccessor = elementValueAccessor): PluginCallback { +export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjector { return field => { - field.trackedElements = []; - field.elementValueAccessor = defaultAccessor; + field.observedElements = []; + field.elementValueAccessor = accessor; const mutationObserver = new MutationObserver(mutations => { - const events: TrackedElementsChangeEvent[] = []; - const { trackedElements } = field; - const [element] = trackedElements; + const events: Event_[] = []; + const { observedElements } = field; + const [element] = observedElements; for (const mutation of mutations) { for (let i = 0; i < mutation.removedNodes.length; ++i) { - const elementIndex = trackedElements.indexOf(mutation.removedNodes.item(i) as Element); + const elementIndex = observedElements.indexOf(mutation.removedNodes.item(i) as Element); if (elementIndex === -1) { continue; } - const element = trackedElements[elementIndex]; + const element = observedElements[elementIndex]; element.removeEventListener('input', changeListener); element.removeEventListener('change', changeListener); - trackedElements.splice(elementIndex, 1); - events.push({ type: EVENT_TRACK, target: field as Field, currentTarget: field as Field, element }); + observedElements.splice(elementIndex, 1); + events.push({ type: EVENT_CHANGE_OBSERVED_ELEMENTS, origin: field, target: field, data: element }); } } - if (trackedElements.length === 0) { + if (observedElements.length === 0) { mutationObserver.disconnect(); - field.refCallback?.(null); - } else if (element !== trackedElements[0]) { - field.refCallback?.(trackedElements[0]); + field.ref?.(null); + } else if (element !== observedElements[0]) { + field.ref?.(observedElements[0]); } dispatchEvents(events); @@ -120,27 +105,27 @@ export function uncontrolledPlugin(defaultAccessor = elementValueAccessor): Plug const changeListener = (event: Event): void => { let value; if ( - field.trackedElements.indexOf(event.target as Element) !== -1 && - !isDeepEqual((value = field.elementValueAccessor.get(field.trackedElements)), field.value) + field.observedElements.indexOf(event.target as Element) !== -1 && + !isDeepEqual((value = field.elementValueAccessor.get(field.observedElements)), field.value) ) { field.setValue(value); } }; - field.on('valueChange', () => { - if (field.trackedElements.length !== 0) { - field.elementValueAccessor.set(field.trackedElements, field.value); + field.on('change:value', () => { + if (field.observedElements.length !== 0) { + field.elementValueAccessor.set(field.observedElements, field.value); } }); - field.track = element => { - const { trackedElements } = field; + field.observe = element => { + const { observedElements } = field; if ( element === null || !(element instanceof Element) || !element.isConnected || - trackedElements.indexOf(element) !== -1 + observedElements.indexOf(element) !== -1 ) { return; } @@ -150,22 +135,19 @@ export function uncontrolledPlugin(defaultAccessor = elementValueAccessor): Plug element.addEventListener('input', changeListener); element.addEventListener('change', changeListener); - trackedElements.push(element); + observedElements.push(element); - field.elementValueAccessor.set(trackedElements, field.value); + field.elementValueAccessor.set(observedElements, field.value); - if (trackedElements.length === 1) { - field.refCallback?.(trackedElements[0]); + if (observedElements.length === 1) { + field.ref?.(observedElements[0]); } - dispatchEvents([{ type: EVENT_TRACK, target: field, currentTarget: field, element }]); + dispatchEvents([{ type: EVENT_CHANGE_OBSERVED_ELEMENTS, origin: field, target: field, data: element }]); }; field.setElementValueAccessor = accessor => { - field.elementValueAccessor = { - get: accessor.get || defaultAccessor.get, - set: accessor.set || defaultAccessor.set, - }; + field.elementValueAccessor = accessor; return field; }; }; diff --git a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts index 9546ac21..5620e377 100644 --- a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts +++ b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts @@ -14,24 +14,24 @@ describe('uncontrolledPlugin', () => { }); test('updates field value on input change', () => { - const listenerMock = jest.fn(); + const subscriberMock = jest.fn(); const field = createField({ foo: 0 }, uncontrolledPlugin()); element.type = 'number'; - field.subscribe(listenerMock); - field.at('foo').refCallback(element); + field.subscribe(subscriberMock); + field.at('foo').ref(element); fireEvent.change(element, { target: { value: '111' } }); - expect(listenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(field.value).toEqual({ foo: 111 }); }); test('updates input value on field change', () => { const field = createField({ foo: 0 }, uncontrolledPlugin()); - field.at('foo').refCallback(element); + field.at('foo').ref(element); field.at('foo').setValue(111); expect(element.value).toBe('111'); @@ -42,15 +42,15 @@ describe('uncontrolledPlugin', () => { element.type = 'number'; - field.at('foo').refCallback(element); + field.at('foo').ref(element); expect(element.value).toBe('111'); }); - test('invokes refCallback from the preceding plugin', () => { - const refCallbackMock = jest.fn(); + test('invokes ref from the preceding plugin', () => { + const refMock = jest.fn(); const pluginMock = jest.fn(field => { - field.refCallback = refCallbackMock; + field.ref = refMock; }); const field = createField({ foo: 111 }, composePlugins(pluginMock, uncontrolledPlugin())); @@ -58,18 +58,18 @@ describe('uncontrolledPlugin', () => { expect(pluginMock).toHaveBeenCalledTimes(1); expect(pluginMock).toHaveBeenNthCalledWith(1, field, naturalAccessor, expect.any(Function)); - expect(refCallbackMock).not.toHaveBeenCalled(); + expect(refMock).not.toHaveBeenCalled(); - field.at('foo').refCallback(element); + field.at('foo').ref(element); - expect(refCallbackMock).toHaveBeenCalledTimes(1); - expect(refCallbackMock).toHaveBeenNthCalledWith(1, element); + expect(refMock).toHaveBeenCalledTimes(1); + expect(refMock).toHaveBeenNthCalledWith(1, element); }); test('does not invoke preceding plugin if an additional element is added', () => { - const refCallbackMock = jest.fn(); + const refMock = jest.fn(); const plugin = (field: any) => { - field.refCallback = refCallbackMock; + field.ref = refMock; }; const element1 = document.body.appendChild(document.createElement('input')); @@ -77,17 +77,17 @@ describe('uncontrolledPlugin', () => { const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').refCallback(element1); - field.at('foo').refCallback(element2); + field.at('foo').ref(element1); + field.at('foo').ref(element2); - expect(refCallbackMock).toHaveBeenCalledTimes(1); - expect(refCallbackMock).toHaveBeenNthCalledWith(1, element1); + expect(refMock).toHaveBeenCalledTimes(1); + expect(refMock).toHaveBeenNthCalledWith(1, element1); }); test('invokes preceding plugin if the head element has changed', done => { - const refCallbackMock = jest.fn(); + const refMock = jest.fn(); const plugin = (field: any) => { - field.refCallback = refCallbackMock; + field.ref = refMock; }; const element1 = document.body.appendChild(document.createElement('input')); @@ -95,52 +95,52 @@ describe('uncontrolledPlugin', () => { const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').refCallback(element1); - field.at('foo').refCallback(element2); + field.at('foo').ref(element1); + field.at('foo').ref(element2); - expect(refCallbackMock).toHaveBeenCalledTimes(1); - expect(refCallbackMock).toHaveBeenNthCalledWith(1, element1); + expect(refMock).toHaveBeenCalledTimes(1); + expect(refMock).toHaveBeenNthCalledWith(1, element1); element1.remove(); queueMicrotask(() => { - expect(refCallbackMock).toHaveBeenCalledTimes(2); - expect(refCallbackMock).toHaveBeenNthCalledWith(2, element2); + expect(refMock).toHaveBeenCalledTimes(2); + expect(refMock).toHaveBeenNthCalledWith(2, element2); done(); }); }); test('invokes preceding plugin if the head element was removed', done => { - const refCallbackMock = jest.fn(); + const refMock = jest.fn(); const plugin = (field: any) => { - field.refCallback = refCallbackMock; + field.ref = refMock; }; const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').refCallback(element); + field.at('foo').ref(element); element.remove(); queueMicrotask(() => { - expect(refCallbackMock).toHaveBeenCalledTimes(2); - expect(refCallbackMock).toHaveBeenNthCalledWith(1, element); - expect(refCallbackMock).toHaveBeenNthCalledWith(2, null); + expect(refMock).toHaveBeenCalledTimes(2); + expect(refMock).toHaveBeenNthCalledWith(1, element); + expect(refMock).toHaveBeenNthCalledWith(2, null); done(); }); }); test('null refs are not propagated to preceding plugin', () => { - const refCallbackMock = jest.fn(); + const refMock = jest.fn(); const plugin = (field: any) => { - field.refCallback = refCallbackMock; + field.ref = refMock; }; const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').refCallback(null); + field.at('foo').ref(null); - expect(refCallbackMock).not.toHaveBeenCalled(); + expect(refMock).not.toHaveBeenCalled(); }); test('does not call setValue if the same value multiple times', () => { @@ -153,7 +153,7 @@ describe('uncontrolledPlugin', () => { const setValueMock = (field.setValue = jest.fn(field.setValue)); - field.refCallback(element); + field.ref(element); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); @@ -178,7 +178,7 @@ describe('uncontrolledPlugin', () => { const field = createField({ foo: 'aaa' }, uncontrolledPlugin(accessorMock)); - field.at('foo').refCallback(element); + field.at('foo').ref(element); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); @@ -213,12 +213,12 @@ describe('uncontrolledPlugin', () => { const field = createField({ foo: 111 }, uncontrolledPlugin(accessorMock)); - field.at('foo').refCallback(element1); + field.at('foo').ref(element1); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element1], 111); - field.at('foo').refCallback(element2); + field.at('foo').ref(element2); expect(accessorMock.set).toHaveBeenCalledTimes(2); expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element1, element2], 111); @@ -234,7 +234,7 @@ describe('uncontrolledPlugin', () => { const field = createField({ foo: 111 }, uncontrolledPlugin(accessorMock)); - field.at('foo').refCallback(element); + field.at('foo').ref(element); expect(accessorMock.set).toHaveBeenCalledTimes(0); }); @@ -244,7 +244,7 @@ describe('uncontrolledPlugin', () => { const field = createField({ foo: 111 }, uncontrolledPlugin()); - field.at('foo').refCallback(element); + field.at('foo').ref(element); element.remove(); diff --git a/packages/zod-plugin/src/main/index.ts b/packages/zod-plugin/src/main/index.ts index 59102772..86b06fe0 100644 --- a/packages/zod-plugin/src/main/index.ts +++ b/packages/zod-plugin/src/main/index.ts @@ -1,5 +1,5 @@ /** - * @module @roqueform/zod-plugin + * @module zod-plugin */ export * from './zodPlugin'; diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index 7f820c72..71a2c7db 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -1,5 +1,5 @@ -import { ParseParams, SafeParseReturnType, ZodIssue, ZodIssueCode, ZodType, ZodTypeAny } from 'zod'; -import { Field, PluginCallback, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; +import { ParseParams, SafeParseReturnType, ZodIssue, ZodIssueCode, ZodSchema, ZodTypeAny } from 'zod'; +import { AnyField, Field, PluginInjector, Validation, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; /** * The plugin added to fields by the {@link zodPlugin}. @@ -7,6 +7,8 @@ import { Field, PluginCallback, ValidationPlugin, validationPlugin, Validator } export interface ZodPlugin extends ValidationPlugin> { /** * The Zod validation schema of the root value. + * + * @protected */ ['schema']: ZodTypeAny; @@ -16,42 +18,49 @@ export interface ZodPlugin extends ValidationPlugin(schema: ZodType): PluginCallback { - let plugin: PluginCallback; +export function zodPlugin(schema: ZodSchema): PluginInjector { + let plugin; return field => { (plugin ||= validationPlugin(zodValidator))(field); - field.schema = schema; + field.schema = field.parent?.schema || schema; const { setError } = field; field.setError = error => { - if (typeof error === 'string') { - error = { code: ZodIssueCode.custom, path: getPath(field), message: error }; - } - setError(error); + setError(typeof error === 'string' ? { code: ZodIssueCode.custom, path: getPath(field), message: error } : error); }; }; } const zodValidator: Validator> = { validate(field, options) { - applyResult(field, (field as unknown as Field).schema.safeParse(getRootValue(field), options)); + const { validation, schema } = field as unknown as Field; + + if (validation !== null) { + applyResult(validation, schema.safeParse(getValue(field), options)); + } }, validateAsync(field, options) { - return (field as unknown as Field).schema.safeParseAsync(getRootValue(field), options).then(result => { - applyResult(field, result); - }); + const { validation, schema } = field as unknown as Field; + + if (validation !== null) { + return schema.safeParseAsync(getValue(field), options).then(result => { + applyResult(validation, result); + }); + } + + return Promise.resolve(); }, }; -function getRootValue(field: Field): unknown { +function getValue(field: Field): unknown { let value = field.value; let transient = false; @@ -63,39 +72,39 @@ function getRootValue(field: Field): unknown { return value; } -function getPath(field: Field): any[] { +function getPath(field: AnyField): any[] { const path = []; - for (let ancestor = field; ancestor.parent !== null; ancestor = ancestor.parent) { - path.unshift(ancestor.key); + + while (field.parent !== null) { + path.unshift(field.key); + field = field.parent; } return path; } -function applyResult(field: Field, result: SafeParseReturnType): void { - const { validation } = field; - - if (validation === null || result.success) { +function applyResult(validation: Validation, result: SafeParseReturnType): void { + if (result.success) { return; } - let prefix = getPath(field); + const basePath = getPath(validation.root); issues: for (const issue of result.error.issues) { const { path } = issue; - let targetField = field; - - if (path.length < prefix.length) { + if (path.length < basePath.length) { continue; } - for (let i = 0; i < prefix.length; ++i) { - if (path[i] !== prefix[i]) { + for (let i = 0; i < basePath.length; ++i) { + if (path[i] !== basePath[i]) { continue issues; } } - for (let i = prefix.length; i < path.length; ++i) { - targetField = targetField.at(path[i]); + + let child = validation.root; + for (let i = basePath.length; i < path.length; ++i) { + child = child.at(path[i]); } - field.setValidationError(validation, issue); + child.setValidationError(validation, issue); } } diff --git a/tsconfig.typedoc.json b/tsconfig.typedoc.json new file mode 100644 index 00000000..c77389ea --- /dev/null +++ b/tsconfig.typedoc.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./packages/*/src/main/**/*" + ] +} diff --git a/typedoc.json b/typedoc.json index 99d1ea18..2a9557b8 100644 --- a/typedoc.json +++ b/typedoc.json @@ -7,7 +7,11 @@ "disableSources": true, "hideGenerator": true, "readme": "none", - "tsconfig": "./tsconfig.json", + "visibilityFilters": { + "protected": true, + "inherited": true + }, + "tsconfig": "./tsconfig.typedoc.json", "customCss": "./node_modules/typedoc-custom-css/custom.css", "entryPoints": [ "./packages/roqueform/src/main/index.ts", From cfa2bbe54eca6156ebb62894583049c061fcadfa Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Thu, 9 Nov 2023 22:46:34 +0300 Subject: [PATCH 11/33] Fixed tests --- README.md | 48 +- .../src/main/constraintValidationPlugin.ts | 64 +-- .../test/constraintValidationPlugin.test.ts | 194 +++---- packages/doubter-plugin/README.md | 19 +- .../doubter-plugin/src/main/doubterPlugin.ts | 22 +- .../src/test/doubterPlugin.test-d.ts | 4 +- .../src/test/doubterPlugin.test.tsx | 192 +++---- packages/react/README.md | 4 +- packages/react/src/main/AccessorContext.ts | 4 +- packages/react/src/main/FieldRenderer.ts | 20 +- packages/react/src/main/useField.ts | 2 +- .../react/src/test/FieldRenderer.test.tsx | 123 ++--- packages/react/src/test/useField.test.ts | 2 +- .../ref-plugin/src/test/refPlugin.test.ts | 12 +- packages/reset-plugin/src/main/resetPlugin.ts | 11 +- .../reset-plugin/src/test/resetPlugin.test.ts | 52 +- packages/roqueform/src/main/composePlugins.ts | 4 +- packages/roqueform/src/main/createField.ts | 33 +- .../roqueform/src/main/naturalAccessor.ts | 12 +- packages/roqueform/src/main/typings.ts | 16 +- packages/roqueform/src/main/utils.ts | 47 +- .../roqueform/src/main/validationPlugin.ts | 19 +- .../src/test/composePlugins.test-d.ts | 8 +- .../roqueform/src/test/createField.test.ts | 423 +++++++-------- .../src/test/naturalAccessor.test.ts | 18 +- packages/roqueform/src/test/utils.test.ts | 57 +- .../src/test/validationPlugin.test-d.ts | 4 +- .../src/test/validationPlugin.test.tsx | 510 ++++++++++-------- packages/scroll-to-error-plugin/README.md | 12 +- .../src/test/scrollToErrorPlugin.test.tsx | 118 ++-- .../src/main/uncontrolledPlugin.ts | 11 +- .../test/createElementValueAccessor.test.ts | 14 +- .../src/test/uncontrolledPlugin.test.ts | 80 +-- packages/zod-plugin/README.md | 11 +- packages/zod-plugin/package.json | 3 +- packages/zod-plugin/src/main/zodPlugin.ts | 2 +- .../zod-plugin/src/test/zodPlugin.test-d.ts | 4 +- .../zod-plugin/src/test/zodPlugin.test.tsx | 135 ++--- typedoc.json | 1 + 39 files changed, 1121 insertions(+), 1194 deletions(-) diff --git a/README.md b/README.md index 2e2d3ab4..4754f813 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ const planetsField = universeField.at('planets'); // ⮕ Field ``` -`planetsField` is a derived field, and it is linked to its parent `universeField`. +`planetsField` is a child field, and it is linked to its parent `universeField`. ```ts planetsField.key; @@ -126,18 +126,18 @@ universeField.at('planets'); // ⮕ planetsField ``` -So most of the time you don't need to store a derived field in a variable if you already have a reference to a parent +So most of the time you don't need to store a child field in a variable if you already have a reference to a parent field. -The derived field has all the same functionality as its parent, so you can derive a new field from it as well: +The child field has all the same functionality as its parent, so you can derive a new field from it as well: ```ts planetsField.at(0).at('name'); // ⮕ Field ``` -When a value is set to a derived field, a parent field value is also updated. If parent field doesn't have a value yet, -Roqueform would infer its type from on the derived field key. +When a value is set to a child field, a parent field value is also updated. If parent field doesn't have a value yet, +Roqueform would infer its type from on the child field key. ```ts universeField.value; @@ -152,7 +152,7 @@ universeField.value; By default, for a string key a parent object is created, and for a number key a parent array is created. You can change this behaviour with [custom accessors](#accessors). -When a value is set to a parent field, derived fields are also updated: +When a value is set to a parent field, child fields are also updated: ```ts const nameField = universeField.at('planets').at(0).at('name'); @@ -171,19 +171,19 @@ nameField.value; You can subscribe a subscriber to a field updates. The returned callback would unsubscribe the subscriber. ```ts -const unsubscribe = planetsField.subscribe((updatedField, currentField) => { +const unsubscribe = planetsField.on('*', (updatedField, currentField) => { // Handle the update }); // ⮕ () => void ``` -Listeners are called with two arguments: +Subscribers are called with two arguments:
updatedField
-The field that initiated the update. In this example it can be `planetsField` itself, any of its derived fields, or any +The field that initiated the update. In this example it can be `planetsField` itself, any of its child fields, or any of its ancestor fields.
@@ -195,15 +195,15 @@ The field to which the subscriber is subscribed. In this example it is `planetsF
-Listeners are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin). -The root field and all derived fields are updated before subscribers are called, so it's safe to read field values in a +Subscribers are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin). +The root field and all child fields are updated before subscribers are called, so it's safe to read field values in a subscriber. Fields use [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison to detect that the value has changed. ```ts -planetsField.at(0).at('name').subscribe(subscriber); +planetsField.at(0).at('name').on('*', subscriber); // ✅ The subscriber is called planetsField.at(0).at('name').setValue('Mercury'); @@ -215,10 +215,10 @@ planetsField.at(0).setValue({ name: 'Mercury' }); # Transient updates When you call [`setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue) on a field -then subscribers of its ancestors and its updated derived fields are triggered. To manually control the update propagation +then subscribers of its ancestors and its updated child fields are triggered. To manually control the update propagation to fields ancestors, you can use transient updates. -When a value of a derived field is set transiently, values of its ancestors _aren't_ immediately updated. +When a value of a child field is set transiently, values of its ancestors _aren't_ immediately updated. ```ts const avatarField = createField(); @@ -240,7 +240,7 @@ avatarField.at('eyeColor').isTransient; // ⮕ true ``` -To propagate the transient value contained by the derived field to its parent, use +To propagate the transient value contained by the child field to its parent, use the [`dispatch`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#dispatch) method: ```ts @@ -253,7 +253,7 @@ avatarField.value; `setTransientValue` can be called multiple times, but only the most recent update is propagated to the parent field after the `dispatch` call. -When a derived field is in a transient state, its value as observed from the parent may differ from the actual value: +When a child field is in a transient state, its value as observed from the parent may differ from the actual value: ```ts const planetsField = createField(['Mars', 'Pluto']); @@ -282,20 +282,20 @@ planetsField.at(1).value; # Accessors -[`Accessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates +[`ValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates field values. -- When the new field is derived via +- When the new field is child via [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#at) method, the field value is read from the value of the parent field using the - [`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. + [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. - When a field value is updated via [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue), then the parent field value is updated with the value returned from the - [`Accessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the - updated field has derived fields, their values are updated with values returned from the - [`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. + [`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the + updated field has child fields, their values are updated with values returned from the + [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. You can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses [`naturalAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalAccessor.html): @@ -375,7 +375,7 @@ planetField.element; // ⮕ null ``` -The plugin would be called for each derived field when it is first accessed: +The plugin would be called for each child field when it is first accessed: ```ts planetField.at('name').element @@ -413,7 +413,7 @@ Now when `setElement` is called on a field, its subscribers would be invoked. ```ts const planetField = createField({ name: 'Mars' }, elementPlugin); -planetField.at('name').subscribe((updatedField, currentField) => { +planetField.at('name').on('*', (updatedField, currentField) => { // Handle the update currentField.element; // ⮕ document.body diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index a049f7cf..0c886de9 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -1,4 +1,4 @@ -import { dispatchEvents, Event as Event_, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform'; +import { dispatchEvents, Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform'; const EVENT_CHANGE_ERROR = 'change:error'; @@ -7,7 +7,7 @@ const EVENT_CHANGE_ERROR = 'change:error'; */ export interface ConstraintValidationPlugin { /** - * An error associated with the field, or `null` if there's no error. + * A non-empty error message associated with the field, or `null` if there's no error. */ error: string | null; @@ -17,7 +17,7 @@ export interface ConstraintValidationPlugin { element: Element | null; /** - * `true` if the field or any of its derived fields have {@link error an associated error}, or `false` otherwise. + * `true` if the field or any of its child fields have {@link error an associated error}, or `false` otherwise. */ readonly isInvalid: boolean; @@ -37,7 +37,7 @@ export interface ConstraintValidationPlugin { /** * The origin of the associated error: * - 0 if there's no associated error. - * - 1 if an error was set using Constraint Validation API; + * - 1 if an error was set by Constraint Validation API; * - 2 if an error was set using {@link ValidationPlugin.setError}; * * @protected @@ -52,24 +52,24 @@ export interface ConstraintValidationPlugin { /** * Associates an error with the field. * - * @param error The error to set. If `null` or `undefined` then an error is deleted. + * @param error The error to set. If `null`, `undefined`, or an empty string then an error is deleted. */ setError(error: string | null | undefined): void; /** * Deletes an error associated with this field. If this field has {@link element an associated element} that supports * [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation) and the - * element has non-empty `validationMessage` then this message would be immediately set as an error for this field. + * element has a non-empty `validationMessage` then this message would be immediately set as an error for this field. */ deleteError(): void; /** - * Recursively deletes errors associated with this field and all of its derived fields. + * Recursively deletes errors associated with this field and all of its child fields. */ clearErrors(): void; /** - * Shows error message balloon for the first element that is associated with this field or any of its derived fields, + * Shows error message balloon for the first element that is associated with this field or any of its child fields, * that has an associated error via calling * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity reportValidity}. * @@ -104,14 +104,17 @@ export interface ConstraintValidationPlugin { */ export function constraintValidationPlugin(): PluginInjector { return field => { - field.error = field.element = field.validity = null; + field.error = null; + field.element = null; + field.validity = null; field.errorCount = 0; + field.errorOrigin = 0; Object.defineProperties(field, { - isInvalid: { get: () => field.errorCount !== 0 }, + isInvalid: { configurable: true, get: () => field.errorCount !== 0 }, }); - const changeListener = (event: Event): void => { + const changeListener: EventListener = event => { if (field.element === event.target && isValidatable(field.element)) { dispatchEvents(setError(field, field.element.validationMessage, 1, [])); } @@ -132,13 +135,13 @@ export function constraintValidationPlugin(): PluginInjector { @@ -172,7 +175,7 @@ export function constraintValidationPlugin(): PluginInjector reportValidity(field); - field.getErrors = () => getErrors(getInvalidFields(field, [])); + field.getErrors = () => getInvalidFields(field, []).map(field => field.error!); field.getInvalidFields = () => getInvalidFields(field, []); }; @@ -192,8 +195,8 @@ function setError( field: Field, error: string | null | undefined, errorOrigin: 1 | 2, - events: Event_[] -): Event_[] { + events: Event[] +): Event[] { if (error === null || error === undefined || error.length === 0) { return deleteError(field, errorOrigin, events); } @@ -228,7 +231,7 @@ function setError( return events; } -function deleteError(field: Field, errorOrigin: 1 | 2, events: Event_[]): Event_[] { +function deleteError(field: Field, errorOrigin: 1 | 2, events: Event[]): Event[] { const originalError = field.error; if (field.errorOrigin > errorOrigin || originalError === null) { @@ -263,7 +266,7 @@ function deleteError(field: Field, errorOrigin: 1 | return events; } -function clearErrors(field: Field, events: Event_[]): Event_[] { +function clearErrors(field: Field, events: Event[]): Event[] { deleteError(field, 2, events); if (field.children !== null) { @@ -300,17 +303,6 @@ function getInvalidFields( return batch; } -function getErrors(batch: Field[]): string[] { - const errors = []; - - for (const field of batch) { - if (field.error !== null) { - errors.push(field.error); - } - } - return errors; -} - function isValidatable(element: Element | null): element is ValidatableElement { return element instanceof Element && 'validity' in element && element.validity instanceof ValidityState; } diff --git a/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts b/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts index 4373d6e8..5001ed3a 100644 --- a/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts +++ b/packages/constraint-validation-plugin/src/test/constraintValidationPlugin.test.ts @@ -14,153 +14,189 @@ describe('constraintValidationPlugin', () => { }); test('enhances the field', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + const field = createField({ aaa: 111 }, constraintValidationPlugin()); + expect(field.error).toBeNull(); + expect(field.element).toBeNull(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); - - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.validity).toBeNull(); + expect(field.errorCount).toBe(0); + expect(field.errorOrigin).toBe(0); + + expect(field.at('aaa').error).toBeNull(); + expect(field.at('aaa').element).toBeNull(); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').validity).toBeNull(); + expect(field.at('aaa').errorCount).toBe(0); + expect(field.at('aaa').errorOrigin).toBe(0); }); test('sets an error to the field that does not have an associated element', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + const field = createField({ aaa: 111 }, constraintValidationPlugin()); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('change:error', subscriberMock); + field.at('aaa').on('change:error', aaaSubscriberMock); - field.at('foo').setError('aaa'); + field.at('aaa').setError('222'); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe('aaa'); + expect(field.error).toBeNull(); + expect(field.errorCount).toBe(1); + expect(field.errorOrigin).toBe(0); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe('222'); + expect(field.at('aaa').errorCount).toBe(1); + expect(field.at('aaa').errorOrigin).toBe(2); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); }); test('setting an error to the parent field does not affect the child field', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + const field = createField({ aaa: 111 }, constraintValidationPlugin()); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('change:error', subscriberMock); + field.at('aaa').on('change:error', aaaSubscriberMock); - field.setError('aaa'); + field.setError('222'); expect(field.isInvalid).toBe(true); - expect(field.error).toBe('aaa'); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.error).toBe('222'); + expect(field.errorCount).toBe(1); + expect(field.errorOrigin).toBe(2); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); + expect(field.at('aaa').errorCount).toBe(0); + expect(field.at('aaa').errorOrigin).toBe(0); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(0); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(0); }); test('does not notify the field if the same error is set', () => { - const field = createField(0, constraintValidationPlugin()); + const field = createField(111, constraintValidationPlugin()); const subscriberMock = jest.fn(); - field.subscribe(subscriberMock); + field.on('*', subscriberMock); - field.setError('aaa'); - field.setError('aaa'); + field.setError('xxx'); + field.setError('xxx'); expect(subscriberMock).toHaveBeenCalledTimes(1); }); test('deletes an error from the field', () => { - const field = createField(0, constraintValidationPlugin()); + const field = createField(111, constraintValidationPlugin()); const subscriberMock = jest.fn(); - field.subscribe(subscriberMock); + field.on('*', subscriberMock); - field.setError('aaa'); + field.setError('xxx'); field.deleteError(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); + expect(field.errorCount).toBe(0); + expect(field.errorOrigin).toBe(0); expect(subscriberMock).toHaveBeenCalledTimes(2); }); - test('clears an error of a derived field', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + test('restores the constraint error on delete', () => { + const field = createField(111, constraintValidationPlugin()); + + element.required = true; + + field.ref(element); + + expect(field.error).toEqual('Constraints not satisfied'); + + field.setError('xxx'); + + expect(field.error).toEqual('xxx'); + + field.deleteError(); + + expect(field.error).toEqual('Constraints not satisfied'); + }); + + test('clears an error of a child field', () => { + const field = createField({ aaa: 111 }, constraintValidationPlugin()); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - field.at('foo').setError('aaa'); + field.at('aaa').setError('aaa'); field.clearErrors(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('reports validity of the root field', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + const field = createField({ aaa: 111 }, constraintValidationPlugin()); expect(field.reportValidity()).toBe(true); - expect(field.at('foo').reportValidity()).toBe(true); + expect(field.at('aaa').reportValidity()).toBe(true); - field.setError('aaa'); + field.setError('xxx'); expect(field.reportValidity()).toBe(false); - expect(field.at('foo').reportValidity()).toBe(true); + expect(field.at('aaa').reportValidity()).toBe(true); field.deleteError(); expect(field.reportValidity()).toBe(true); - expect(field.at('foo').reportValidity()).toBe(true); + expect(field.at('aaa').reportValidity()).toBe(true); }); - test('reports validity of the derived field', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + test('reports validity of the child field', () => { + const field = createField({ aaa: 111 }, constraintValidationPlugin()); - field.at('foo').setError('aaa'); + field.at('aaa').setError('xxx'); expect(field.reportValidity()).toBe(false); - expect(field.at('foo').reportValidity()).toBe(false); + expect(field.at('aaa').reportValidity()).toBe(false); - field.at('foo').deleteError(); + field.at('aaa').deleteError(); expect(field.reportValidity()).toBe(true); - expect(field.at('foo').reportValidity()).toBe(true); + expect(field.at('aaa').reportValidity()).toBe(true); }); test('uses a validationMessage as an error', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + const field = createField({ aaa: 111 }, constraintValidationPlugin()); element.required = true; - field.at('foo').ref(element); + field.at('aaa').ref(element); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual('Constraints not satisfied'); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual('Constraints not satisfied'); }); test('deletes an error when a ref is changed', () => { - const field = createField(0, constraintValidationPlugin()); + const field = createField(111, constraintValidationPlugin()); const subscriberMock = jest.fn(); - field.subscribe(subscriberMock); + field.on('*', subscriberMock); element.required = true; @@ -173,44 +209,18 @@ describe('constraintValidationPlugin', () => { field.ref(null); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); expect(subscriberMock).toHaveBeenCalledTimes(2); }); test('notifies the field when the value is changed', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); - - const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); - - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); - - element.value = 'aaa'; - element.required = true; - - expect(element.validationMessage).toBe(''); - expect(subscriberMock).not.toHaveBeenCalled(); - - field.at('foo').ref(element); - - expect(subscriberMock).toHaveBeenCalledTimes(0); - expect(field.at('foo').isInvalid).toBe(false); - - fireEvent.change(element, { target: { value: '' } }); - - expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - }); - - test('does not notify an already invalid parent', () => { - const field = createField({ foo: 0 }, constraintValidationPlugin()); + const field = createField({ aaa: 111 }, constraintValidationPlugin()); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('change:error', subscriberMock); + field.at('aaa').on('change:error', aaaSubscriberMock); element.value = 'aaa'; element.required = true; @@ -218,14 +228,14 @@ describe('constraintValidationPlugin', () => { expect(element.validationMessage).toBe(''); expect(subscriberMock).not.toHaveBeenCalled(); - field.at('foo').ref(element); + field.at('aaa').ref(element); expect(subscriberMock).toHaveBeenCalledTimes(0); - expect(field.at('foo').isInvalid).toBe(false); + expect(field.at('aaa').isInvalid).toBe(false); fireEvent.change(element, { target: { value: '' } }); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/doubter-plugin/README.md b/packages/doubter-plugin/README.md index a1b92220..d60fc439 100644 --- a/packages/doubter-plugin/README.md +++ b/packages/doubter-plugin/README.md @@ -35,13 +35,12 @@ export const App = () => { event.preventDefault(); if (planetField.validate()) { - // Errors are associated with fields automatically - return; + // If your shapes transform the input, you can safely parse + // the field value after it was successfully validated. + const value = planetShape.parse(planetField.value); + } else { + // Errors are associated with fields automatically. } - - // If your shapes transform the input, you can safely parse - // the field value after it was successfully validated - const value = planetShape.parse(planetField.value); }; return ( @@ -96,8 +95,8 @@ const planetField = useField({ name: 'Mars' }, doubterPlugin(planetShape)); The type of the field value is inferred from the provided shape, so the field value is statically checked. -When you call the `validate` method, it triggers validation of the field and all of its derived fields. So if you call -`validate` on the derived field, it won't validate the parent field: +When you call the `validate` method, it triggers validation of the field and all of its child fields. So if you call +`validate` on the child field, it won't validate the parent field: ```ts planetField.at('name').validate(); @@ -110,7 +109,7 @@ In this example, `planetField.value` _is not_ validated, and `planetField.at('na > It's safe to trigger validation of a single text field on every keystroke, since validation doesn't have to process > the whole form state. -To detect whether the field, or any of its derived fields contain a validation error: +To detect whether the field, or any of its child fields contain a validation error: ```ts planetField.isInvalid; @@ -140,7 +139,7 @@ To delete an error for the particular field: planetField.at('name').deleteError(); ``` -Sometimes it is required to clear errors of the field itself and all of its derived fields: +Sometimes it is required to clear errors of the field itself and all of its child fields: ```ts planetField.clearErrors(); diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index c3cc482e..9b4416fc 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -33,7 +33,11 @@ export function doubterPlugin(shape: Shape): PluginInjector { - setError(typeof error === 'string' ? { message: error, path: getPath(field) } : error); + if (error === null || error === undefined) { + setError(error); + } else { + setError(prependPath(field, typeof error === 'string' ? { message: error, input: field.value } : error)); + } }; }; } @@ -60,14 +64,12 @@ const doubterValidator: Validator = { }, }; -function getPath(field: AnyField): any[] { - const path = []; - +function prependPath(field: AnyField, issue: Issue): Issue { while (field.parent !== null) { - path.unshift(field.key); + (issue.path ||= []).unshift(field.key); field = field.parent; } - return path; + return issue; } function applyResult(validation: Validation, result: Err | Ok): void { @@ -75,8 +77,6 @@ function applyResult(validation: Validation, result: Err | Ok): v return; } - const basePath = getPath(validation.root); - for (const issue of result.issues) { let child = validation.root; @@ -84,11 +84,7 @@ function applyResult(validation: Validation, result: Err | Ok): v for (const key of issue.path) { child = child.at(key); } - issue.path = basePath.concat(issue.path); - } else { - issue.path = basePath.slice(0); } - - child.setValidationError(validation, issue); + child.setValidationError(validation, prependPath(validation.root, issue)); } } diff --git a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts index a3bb66ba..afd2d4fe 100644 --- a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts +++ b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts @@ -5,6 +5,4 @@ import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin'; const shape = d.object({ foo: d.object({ bar: d.string() }) }); -expectType & DoubterPlugin>( - createField({ foo: { bar: 'aaa' } }, doubterPlugin(shape)) -); +expectType>(createField({ foo: { bar: 'aaa' } }, doubterPlugin(shape))); diff --git a/packages/doubter-plugin/src/test/doubterPlugin.test.tsx b/packages/doubter-plugin/src/test/doubterPlugin.test.tsx index 53fb726e..a2374e9c 100644 --- a/packages/doubter-plugin/src/test/doubterPlugin.test.tsx +++ b/packages/doubter-plugin/src/test/doubterPlugin.test.tsx @@ -3,147 +3,147 @@ import { doubterPlugin } from '../main'; import { createField } from 'roqueform'; describe('doubterPlugin', () => { - const fooShape = d.object({ - foo: d.number().gte(3), + const aaaShape = d.object({ + aaa: d.number().gte(3), }); - const fooBarShape = d.object({ - foo: d.number().gte(3), - bar: d.string().max(2), + const aaaBbbShape = d.object({ + aaa: d.number().gte(3), + bbb: d.string().max(2), }); test('enhances the field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('sets an issue to the root field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); - const issue = { code: 'aaa' }; + const issue = { code: 'xxx' }; field.setError(issue); expect(field.isInvalid).toBe(true); expect(field.error).toBe(issue); - expect(field.error).toEqual({ code: 'aaa', input: { foo: 0 } }); + expect(field.error).toEqual({ code: 'xxx' }); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('sets an issue to the child field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); const issue = { code: 'aaa' }; - field.at('foo').setError(issue); + field.at('aaa').setError(issue); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(issue); - expect(field.at('foo').error).toEqual({ code: 'aaa', input: 0, path: ['foo'] }); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(issue); + expect(field.at('aaa').error).toEqual({ code: 'aaa', path: ['aaa'] }); }); test('converts string errors to issue messages', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); - field.at('foo').setError('aaa'); + field.at('aaa').setError('xxx'); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ message: 'aaa', input: 0, path: ['foo'] }); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ message: 'xxx', input: 0, path: ['aaa'] }); }); test('deletes an issue from the root field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); field.setError({ code: 'aaa' }); field.deleteError(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('deletes an issue from the child field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); - field.at('foo').setError({ code: 'aaa' }); - field.at('foo').deleteError(); + field.at('aaa').setError({ code: 'aaa' }); + field.at('aaa').deleteError(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('deletes an issue from the child field but parent remains invalid', () => { - const field = createField({ foo: 0, bar: 'qux' }, doubterPlugin(fooBarShape)); + const field = createField({ aaa: 0, bbb: 'ccc' }, doubterPlugin(aaaBbbShape)); const issue1 = { code: 'aaa' }; const issue2 = { code: 'bbb' }; - field.at('foo').setError(issue1); - field.at('bar').setError(issue2); + field.at('aaa').setError(issue1); + field.at('bbb').setError(issue2); - field.at('bar').deleteError(); + field.at('bbb').deleteError(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(issue1); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(issue1); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); }); test('clears all issues', () => { - const field = createField({ foo: 0, bar: 'qux' }, doubterPlugin(fooBarShape)); + const field = createField({ aaa: 0, bbb: 'ccc' }, doubterPlugin(aaaBbbShape)); const issue1 = { code: 'aaa' }; const issue2 = { code: 'bbb' }; - field.at('foo').setError(issue1); - field.at('bar').setError(issue2); + field.at('aaa').setError(issue1); + field.at('bbb').setError(issue2); field.clearErrors(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); }); test('validates the root field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'number.gte', - path: ['foo'], + path: ['aaa'], input: 0, param: 3, message: 'Must be greater than or equal to 3', @@ -152,17 +152,17 @@ describe('doubterPlugin', () => { }); test('validates the child field', () => { - const field = createField({ foo: 0 }, doubterPlugin(fooShape)); + const field = createField({ aaa: 0 }, doubterPlugin(aaaShape)); - field.at('foo').validate(); + field.at('aaa').validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'number.gte', - path: ['foo'], + path: ['aaa'], input: 0, param: 3, message: 'Must be greater than or equal to 3', @@ -171,28 +171,28 @@ describe('doubterPlugin', () => { }); test('validates multiple fields', () => { - const field = createField({ foo: 0, bar: 'qux' }, doubterPlugin(fooBarShape)); + const field = createField({ aaa: 0, bbb: 'ccc' }, doubterPlugin(aaaBbbShape)); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'number.gte', - path: ['foo'], + path: ['aaa'], input: 0, param: 3, message: 'Must be greater than or equal to 3', meta: undefined, }); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toEqual({ + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toEqual({ code: 'string.max', - path: ['bar'], - input: 'qux', + path: ['bbb'], + input: 'ccc', param: 2, message: 'Must have the maximum length of 2', meta: undefined, @@ -200,78 +200,78 @@ describe('doubterPlugin', () => { }); test('validate clears previous validation issues', () => { - const field = createField({ foo: 0, bar: 'qux' }, doubterPlugin(fooBarShape)); + const field = createField({ aaa: 0, bbb: 'ccc' }, doubterPlugin(aaaBbbShape)); field.validate(); - field.at('bar').setValue(''); + field.at('bbb').setValue(''); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'number.gte', - path: ['foo'], + path: ['aaa'], input: 0, param: 3, message: 'Must be greater than or equal to 3', meta: undefined, }); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); }); test('validate does not clear an issue set from userland', () => { - const field = createField({ foo: 0, bar: '' }, doubterPlugin(fooBarShape)); + const field = createField({ aaa: 0, bbb: '' }, doubterPlugin(aaaBbbShape)); const issue = { code: 'aaa' }; - field.at('bar').setError(issue); + field.at('bbb').setError(issue); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'number.gte', - path: ['foo'], + path: ['aaa'], input: 0, param: 3, message: 'Must be greater than or equal to 3', meta: undefined, }); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toBe(issue); + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toBe(issue); }); test('validate does not raise issues for transient fields', () => { - const field = createField({ foo: 0, bar: 'qux' }, doubterPlugin(fooBarShape)); + const field = createField({ aaa: 0, bbb: 'ccc' }, doubterPlugin(aaaBbbShape)); - field.at('bar').setTransientValue('aaabbb'); + field.at('bbb').setTransientValue('aaabbb'); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'number.gte', - path: ['foo'], + path: ['aaa'], input: 0, param: 3, message: 'Must be greater than or equal to 3', meta: undefined, }); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); }); }); diff --git a/packages/react/README.md b/packages/react/README.md index 708dbc8f..ac0782e7 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -112,7 +112,7 @@ argument of the `children` render function is the field passed as a `field` prop ## Eager and lazy re-renders Let's consider the form with two `FieldRenderer` elements. One of them renders the value of the root field and the other -one renders an input that updates the derived field: +one renders an input that updates the child field: ```tsx const App = () => { @@ -139,7 +139,7 @@ const App = () => { ``` By default, `FieldRenderer` component re-renders only when the provided field was updated directly, so updates from -ancestors or derived fields would be ignored. Add the `eagerlyUpdated` property to force `FieldRenderer` to re-render +ancestors or child fields would be ignored. Add the `eagerlyUpdated` property to force `FieldRenderer` to re-render whenever its value was affected. ```diff diff --git a/packages/react/src/main/AccessorContext.ts b/packages/react/src/main/AccessorContext.ts index 0d2a0625..81155911 100644 --- a/packages/react/src/main/AccessorContext.ts +++ b/packages/react/src/main/AccessorContext.ts @@ -1,9 +1,9 @@ import { createContext, Context } from 'react'; -import { naturalAccessor, Accessor } from 'roqueform'; +import { naturalAccessor, ValueAccessor } from 'roqueform'; /** * The context that is used by {@link useField} to retrieve an accessor. */ -export const AccessorContext: Context = createContext(naturalAccessor); +export const AccessorContext: Context = createContext(naturalAccessor); AccessorContext.displayName = 'AccessorContext'; diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index 44de34c7..356aa54f 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -4,18 +4,18 @@ import { AnyField, callOrGet, ValueOf } from 'roqueform'; /** * Properties of the {@link FieldRenderer} component. * - * @template Field The rendered field. + * @template RenderedField The rendered field. */ -export interface FieldRendererProps { +export interface FieldRendererProps { /** - * The field to subscribe to. + * The field that triggers re-renders. */ - field: Field; + field: RenderedField; /** - * The render function that receive a field as an argument. + * The render function that receive a rendered field as an argument. */ - children: (field: Field) => ReactNode; + children: (field: RenderedField) => ReactNode; /** * If set to `true` then {@link FieldRenderer} is re-rendered whenever the {@link field} itself, its parent fields or @@ -31,19 +31,19 @@ export interface FieldRendererProps { * * @param value The new field value. */ - onChange?: (value: ValueOf) => void; + onChange?: (value: ValueOf) => void; } /** * The component that subscribes to the {@link Field} instance and re-renders its children when the field is notified. * - * @template Field The rendered field. + * @template RenderedField The rendered field. */ -export function FieldRenderer(props: FieldRendererProps): ReactElement { +export function FieldRenderer(props: FieldRendererProps): ReactElement { const { field, eagerlyUpdated } = props; const [, rerender] = useReducer(reduceCount, 0); - const handleChangeRef = useRef['onChange']>(); + const handleChangeRef = useRef['onChange']>(); handleChangeRef.current = props.onChange; diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index b2a96746..5e89af23 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -26,7 +26,7 @@ export function useField(initialValue: Value | (() => Value)): Field { test('passes the field as an argument', () => { @@ -21,82 +21,41 @@ describe('FieldRenderer', () => { ); }); - test('re-renders if field value is changed externally', async () => { + test('re-renders if field value is changed', async () => { const renderMock = jest.fn(); + const rootField = createField(); - let rootField!: Field; - - render( - createElement(() => { - rootField = useField(); - - return {renderMock}; - }) - ); + render(createElement(() => {renderMock})); await act(() => rootField.setValue(111)); expect(renderMock).toHaveBeenCalledTimes(2); }); - test('re-renders if field is notified', async () => { + test('does not re-render if child field value is changed', async () => { const renderMock = jest.fn(); - const plugin: Plugin = (_field, _accessor, notify) => { - notifyCallback = notify; - }; - - let rootField!: Field; - let notifyCallback!: () => void; - - render( - createElement(() => { - rootField = useField(undefined, plugin); - - return {renderMock}; - }) - ); - - await act(() => notifyCallback()); - - expect(renderMock).toHaveBeenCalledTimes(2); - }); - - test('does not re-render if derived field value is changed externally', async () => { - const renderMock = jest.fn(); - - let rootField!: Field<{ foo: number }>; - - render( - createElement(() => { - rootField = useField({ foo: 111 }); + const rootField = createField(); - return {renderMock}; - }) - ); + render(createElement(() => {renderMock})); await act(() => rootField.at('foo').setValue(222)); expect(renderMock).toHaveBeenCalledTimes(1); }); - test('does not re-render if eagerlyUpdated and derived field value is changed externally', async () => { + test('does not re-render if eagerlyUpdated and child field value is changed', async () => { const renderMock = jest.fn(); - - let rootField!: Field<{ foo: number }>; + const rootField = createField(); render( - createElement(() => { - rootField = useField({ foo: 111 }); - - return ( - - {renderMock} - - ); - }) + createElement(() => ( + + {renderMock} + + )) ); await act(() => rootField.at('foo').setValue(222)); @@ -106,22 +65,17 @@ describe('FieldRenderer', () => { test('triggers onChange handler when value is changed non-transiently', async () => { const handleChangeMock = jest.fn(); - - let rootField!: Field<{ foo: number }>; + const rootField = createField(); render( - createElement(() => { - rootField = useField({ foo: 111 }); - - return ( - - {() => null} - - ); - }) + createElement(() => ( + + {() => null} + + )) ); await act(() => rootField.at('foo').setValue(222)); @@ -132,22 +86,17 @@ describe('FieldRenderer', () => { test('triggers onChange handler when value is changed transiently', async () => { const handleChangeMock = jest.fn(); - - let rootField!: Field<{ foo: number }>; + const rootField = createField(); render( - createElement(() => { - rootField = useField({ foo: 111 }); - - return ( - - {() => null} - - ); - }) + createElement(() => ( + + {() => null} + + )) ); await act(() => { @@ -157,7 +106,7 @@ describe('FieldRenderer', () => { expect(handleChangeMock).toHaveBeenCalledTimes(0); - await act(() => rootField.at('foo').dispatch()); + await act(() => rootField.at('foo').propagate()); expect(handleChangeMock).toHaveBeenCalledTimes(1); expect(handleChangeMock).toHaveBeenNthCalledWith(1, 333); diff --git a/packages/react/src/test/useField.test.ts b/packages/react/src/test/useField.test.ts index 5f1aa11d..6f10d787 100644 --- a/packages/react/src/test/useField.test.ts +++ b/packages/react/src/test/useField.test.ts @@ -5,7 +5,7 @@ describe('useField', () => { test('returns field with undefined initial value', () => { const hook = renderHook(() => useField()); - expect(hook.result.current.value).toBe(undefined); + expect(hook.result.current.value).toBeUndefined(); }); test('returns a field with a literal initial value', () => { diff --git a/packages/ref-plugin/src/test/refPlugin.test.ts b/packages/ref-plugin/src/test/refPlugin.test.ts index c55345d6..fa47b28f 100644 --- a/packages/ref-plugin/src/test/refPlugin.test.ts +++ b/packages/ref-plugin/src/test/refPlugin.test.ts @@ -3,14 +3,14 @@ import { refPlugin } from '../main'; describe('refPlugin', () => { test('adds an element property to the field', () => { - const field = createField({ bar: 111 }, refPlugin()); + const field = createField({ aaa: 111 }, refPlugin()); - expect(field.element).toBe(null); - expect(field.at('bar').element).toBe(null); + expect(field.element).toBeNull(); + expect(field.at('aaa').element).toBeNull(); }); test('ref updates an element property', () => { - const field = createField({ bar: 111 }, refPlugin()); + const field = createField({ aaa: 111 }, refPlugin()); const element = document.createElement('input'); field.ref(element); @@ -19,10 +19,10 @@ describe('refPlugin', () => { }); test('preserves the ref from preceding plugin', () => { - const refMock = jest.fn(() => undefined); + const refMock = jest.fn(); const field = createField( - { bar: 111 }, + { aaa: 111 }, composePlugins(field => Object.assign(field, { ref: refMock }), refPlugin()) ); diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index befbdb78..658497ed 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -51,6 +51,11 @@ export function resetPlugin( equalityChecker: (initialValue: any, value: any) => boolean = isDeepEqual ): PluginInjector { return field => { + Object.defineProperty(field, 'isDirty', { + configurable: true, + get: () => !field.equalityChecker(field.initialValue, field.value), + }); + field.equalityChecker = equalityChecker; field.setInitialValue = value => { @@ -60,8 +65,6 @@ export function resetPlugin( field.reset = () => { field.setValue(field.initialValue); }; - - Object.defineProperty(field, 'isDirty', { get: () => field.equalityChecker(field.initialValue, field.value) }); }; } @@ -73,7 +76,7 @@ function setInitialValue(field: Field, initialValue: unknown): void let root = field; while (root.parent !== null) { - initialValue = field.accessor.set(root.parent.value, root.key, initialValue); + initialValue = field.valueAccessor.set(root.parent.value, root.key, initialValue); root = root.parent; } @@ -92,7 +95,7 @@ function propagateInitialValue( if (field.children !== null) { for (const child of field.children) { - const childInitialValue = field.accessor.get(initialValue, child.key); + const childInitialValue = field.valueAccessor.get(initialValue, child.key); if (child !== target && isEqual(child.initialValue, childInitialValue)) { continue; } diff --git a/packages/reset-plugin/src/test/resetPlugin.test.ts b/packages/reset-plugin/src/test/resetPlugin.test.ts index 4d82c26e..9262d0e2 100644 --- a/packages/reset-plugin/src/test/resetPlugin.test.ts +++ b/packages/reset-plugin/src/test/resetPlugin.test.ts @@ -3,82 +3,82 @@ import { resetPlugin } from '../main'; describe('resetPlugin', () => { test('field is dirty if the field value is not equal to an initial value', () => { - const initialValue = { foo: 111 }; + const initialValue = { aaa: 111 }; const field = createField(initialValue, resetPlugin()); expect(field.initialValue).toBe(initialValue); - expect(field.at('foo').initialValue).toBe(111); + expect(field.at('aaa').initialValue).toBe(111); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); - expect(field.at('foo').isDirty).toBe(true); + expect(field.at('aaa').isDirty).toBe(true); expect(field.isDirty).toBe(true); field.setValue(initialValue); - expect(field.at('foo').isDirty).toBe(false); + expect(field.at('aaa').isDirty).toBe(false); expect(field.isDirty).toBe(false); }); test('field is not dirty it has the value that is deeply equal to the initial value', () => { - const initialValue = { foo: 111 }; + const initialValue = { aaa: 111 }; const field = createField(initialValue, resetPlugin()); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); - expect(field.at('foo').isDirty).toBe(true); + expect(field.at('aaa').isDirty).toBe(true); expect(field.isDirty).toBe(true); - field.setValue({ foo: 111 }); + field.setValue({ aaa: 111 }); - expect(field.at('foo').isDirty).toBe(false); + expect(field.at('aaa').isDirty).toBe(false); expect(field.isDirty).toBe(false); }); test('updates the initial value and notifies fields', () => { const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - const initialValue = { foo: 111 }; + const initialValue = { aaa: 111 }; const field = createField(initialValue, resetPlugin()); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('change:initialValue', subscriberMock); + field.at('aaa').on('change:initialValue', aaaSubscriberMock); - const initialValue2 = { foo: 222 }; + const initialValue2 = { aaa: 222 }; field.setInitialValue(initialValue2); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(field.at('foo').initialValue).toBe(222); - expect(field.at('foo').isDirty).toBe(true); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(field.at('aaa').initialValue).toBe(222); + expect(field.at('aaa').isDirty).toBe(true); expect(field.initialValue).toBe(initialValue2); expect(field.isDirty).toBe(true); }); - test('derived field is dirty if its value was updated before the Field instance was created', () => { - const field = createField({ foo: 111 }, resetPlugin()); + test('child field is dirty if its value was updated before the Field instance was created', () => { + const field = createField({ aaa: 111 }, resetPlugin()); - field.setValue({ foo: 222 }); + field.setValue({ aaa: 222 }); - expect(field.at('foo').isDirty).toBe(true); + expect(field.at('aaa').isDirty).toBe(true); }); test('resets to the initial value', () => { - const field = createField({ foo: 111 }, resetPlugin()); + const field = createField({ aaa: 111 }, resetPlugin()); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); expect(field.isDirty).toBe(true); - expect(field.at('foo').isDirty).toBe(true); + expect(field.at('aaa').isDirty).toBe(true); field.reset(); - expect(field.at('foo').isDirty).toBe(false); + expect(field.at('aaa').isDirty).toBe(false); expect(field.isDirty).toBe(false); }); }); diff --git a/packages/roqueform/src/main/composePlugins.ts b/packages/roqueform/src/main/composePlugins.ts index 4312676a..5dd4c505 100644 --- a/packages/roqueform/src/main/composePlugins.ts +++ b/packages/roqueform/src/main/composePlugins.ts @@ -51,8 +51,8 @@ export function composePlugins( /** * Composes multiple plugin callbacks into a single callback. * - * @param plugins The array of plugins to compose. - * @returns The plugins callbacks that sequentially applies all provided plugins to a field. + * @param plugins The array of plugin injectors to compose. + * @returns The plugins injector that sequentially applies all provided injectors into a field. */ export function composePlugins(...plugins: PluginInjector[]): PluginInjector; diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 0cd98746..bd1fd3df 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,4 +1,4 @@ -import { Accessor, Field, Event, PluginInjector } from './typings'; +import { ValueAccessor, Event, Field, PluginInjector, Subscriber } from './typings'; import { callOrGet, dispatchEvents, isEqual } from './utils'; import { naturalAccessor } from './naturalAccessor'; @@ -16,27 +16,27 @@ export function createField(): Field; * Creates the new field instance. * * @param initialValue The initial value assigned to the field. - * @param accessor Resolves values for derived fields. + * @param accessor Resolves values for child fields. * @template Value The root field value. */ -export function createField(initialValue: Value, accessor?: Accessor): Field; +export function createField(initialValue: Value, accessor?: ValueAccessor): Field; /** * Creates the new field instance. * * @param initialValue The initial value assigned to the field. - * @param plugin The plugin that enhances the field. - * @param accessor Resolves values for derived fields. + * @param plugin The plugin injected into the field. + * @param accessor Resolves values for child fields. * @template Value The root field initial value. * @template Plugin The plugin injected into the field. */ export function createField( initialValue: Value, plugin: PluginInjector>, - accessor?: Accessor + accessor?: ValueAccessor ): Field; -export function createField(initialValue?: unknown, plugin?: PluginInjector | Accessor, accessor?: Accessor) { +export function createField(initialValue?: unknown, plugin?: PluginInjector | ValueAccessor, accessor?: ValueAccessor) { if (typeof plugin !== 'function') { accessor = plugin; plugin = undefined; @@ -45,7 +45,7 @@ export function createField(initialValue?: unknown, plugin?: PluginInjector | Ac } function getOrCreateField( - accessor: Accessor, + accessor: ValueAccessor, parent: Field | null, key: unknown, initialValue: unknown, @@ -59,7 +59,7 @@ function getOrCreateField( child = { key, - value: null, + value: initialValue, initialValue, isTransient: false, root: null!, @@ -67,7 +67,7 @@ function getOrCreateField( children: null, childrenMap: null, subscribers: null, - accessor, + valueAccessor: accessor, plugin, setValue: value => { @@ -82,11 +82,14 @@ function getOrCreateField( setValue(child, child.value, false); }, - at: key => getOrCreateField(child.accessor, child, key, null, plugin), + at: key => getOrCreateField(child.valueAccessor, child, key, null, plugin), on: (type, subscriber) => { - let subscribers: unknown[]; - (subscribers = (child.subscribers ||= Object.create(null))[type] ||= []).push(subscriber); + const subscribers: Subscriber[] = ((child.subscribers ||= Object.create(null))[type] ||= []); + + if (!subscribers.includes(subscriber)) { + subscribers.push(subscriber); + } return () => { subscribers.splice(subscribers.indexOf(subscriber), 1); }; @@ -121,7 +124,7 @@ function setValue(field: Field, value: unknown, transient: boolean): void { let changeRoot = field; while (changeRoot.parent !== null && !changeRoot.isTransient) { - value = field.accessor.set(changeRoot.parent.value, changeRoot.key, value); + value = field.valueAccessor.set(changeRoot.parent.value, changeRoot.key, value); changeRoot = changeRoot.parent; } @@ -139,7 +142,7 @@ function propagateValue(origin: Field, field: Field, value: unknown, events: Eve continue; } - const childValue = field.accessor.get(value, child.key); + const childValue = field.valueAccessor.get(value, child.key); if (child !== origin && isEqual(child.value, childValue)) { continue; } diff --git a/packages/roqueform/src/main/naturalAccessor.ts b/packages/roqueform/src/main/naturalAccessor.ts index 41210c79..5e4a6452 100644 --- a/packages/roqueform/src/main/naturalAccessor.ts +++ b/packages/roqueform/src/main/naturalAccessor.ts @@ -1,10 +1,10 @@ -import { Accessor } from './typings'; +import { ValueAccessor } from './typings'; import { isEqual } from './utils'; /** * The accessor that reads and writes key-value pairs to well-known object instances. */ -export const naturalAccessor: Accessor = { +export const naturalAccessor: ValueAccessor = { get(obj, key) { if (isPrimitive(obj)) { return undefined; @@ -93,7 +93,7 @@ export const naturalAccessor: Accessor = { }; /** - * Converts `k` to a number if it represents a valid array index, or returns -1 if `k` isn't an index. + * Converts `k` to a non-negative integer if it represents a valid array index, or returns -1 if `k` isn't an index. */ function toArrayIndex(k: any): number { return (typeof k === 'number' || (typeof k === 'string' && k === '' + (k = +k))) && k >>> 0 === k ? k : -1; @@ -104,13 +104,13 @@ function isPrimitive(obj: any): boolean { obj === null || obj === undefined || typeof obj !== 'object' || + obj instanceof Date || + obj instanceof RegExp || obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj instanceof BigInt || - obj instanceof Symbol || - obj instanceof Date || - obj instanceof RegExp + obj instanceof Symbol ); } diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 11a062a1..04c36b15 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -58,7 +58,7 @@ export type Subscriber = (event: Event void; /** - * Infers the plugin that was used to enhance the field. + * Infers plugins that were injected into a field * * Use `PluginOf` in plugin interfaces to infer all plugin interfaces that were intersected with the field * controller. @@ -155,7 +155,7 @@ export interface FieldController { * @see {@link on} * @protected */ - ['subscribers']: Record[]> | null; + ['subscribers']: Record[] | undefined> | null; /** * The accessor that reads the field value from the value of the parent fields, and updates parent value. @@ -163,7 +163,7 @@ export interface FieldController { * @see [Accessors](https://github.com/smikhalevski/roqueform#accessors) * @protected */ - ['accessor']: Accessor; + ['valueAccessor']: ValueAccessor; /** * The plugin that is applied to this field and all child fields when they are accessed, or `null` field isn't @@ -239,7 +239,7 @@ export type PluginInjector = (field: Field = */ // prettier-ignore type ValueAt = - T extends { set(key: any, value: any): any, get(key: infer K): infer V } ? Key extends K ? V : never : - T extends { add(value: infer V): any, [Symbol.iterator]: Function } ? Key extends number ? V | undefined : never : - T extends readonly any[] ? Key extends number ? T[Key] : never : + T extends { set(key: any, value: any): any, get(key: infer K): infer V } ? Key extends K ? V : undefined : + T extends { add(value: infer V): any, [Symbol.iterator]: Function } ? Key extends number ? V | undefined : undefined : + T extends readonly any[] ? Key extends number ? T[Key] : undefined : Key extends keyof T ? T[Key] : - never + undefined diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 1a29ed1c..0cb52e57 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,9 +1,5 @@ import { Event } from './typings'; -export function callOrGet(value: T | ((prevValue: A) => T), prevValue: A): T { - return typeof value === 'function' ? (value as Function)(prevValue) : value; -} - /** * [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison operation. * @@ -15,26 +11,43 @@ export function isEqual(a: unknown, b: unknown): boolean { return a === b || (a !== a && b !== b); } +/** + * If value is a function then it is called with the given argument, otherwise the value is returned as is. + * + * @param value The value to return or a callback to call. + * @param arg The of argument to pass to the value callback. + * @returns The value or the call result. + * @template T The returned value. + * @template A The value callback argument. + */ +export function callOrGet(value: T | ((arg: A) => T), arg: A): T { + return typeof value === 'function' ? (value as Function)(arg) : value; +} + +/** + * Calls field subscribers that can handle given events. + * + * @param events The array of events to dispatch. + */ export function dispatchEvents(events: readonly Event[]): void { for (const event of events) { const { subscribers } = event.target; - if (subscribers !== null) { - callAll(subscribers[event.type], event); - callAll(subscribers['*'], event); + if (subscribers === null) { + continue; } - } -} -function callAll(subscribers: Array<(event: Event) => void> | undefined, event: Event): void { - if (subscribers !== undefined) { - for (const subscriber of subscribers) { - try { + const typeSubscribers = subscribers[event.type]; + const globSubscribers = subscribers['*']; + + if (typeSubscribers !== undefined) { + for (const subscriber of typeSubscribers) { + subscriber(event); + } + } + if (globSubscribers !== undefined) { + for (const subscriber of globSubscribers) { subscriber(event); - } catch (error) { - setTimeout(() => { - throw error; - }, 0); } } } diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index cbe3efab..55e9708a 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -88,7 +88,7 @@ export interface ValidationPlugin { deleteError(): void; /** - * Recursively deletes errors associated with this field and all of its derived fields. + * Recursively deletes errors associated with this field and all of its child fields. */ clearErrors(): void; @@ -194,13 +194,10 @@ export interface Validator { validate(field: Field>, options: Options | undefined): void; /** - * Applies validation rules to a field. - * - * Check that {@link Validation.abortController validation isn't aborted} before - * {@link ValidationPlugin.setValidationError setting a validation error}, otherwise stop validation as soon as - * possible. + * Applies validation rules to a field. If this callback is omitted, then {@link validate} would be called instead. * - * If this callback is omitted, then {@link validate} would be called instead. + * Set {@link ValidationPlugin.setValidationError validation errors} to invalid fields during validation. Refer to + * {@link ValidationPlugin.validation} to check that validation wasn't aborted. * * @param field The field where {@link ValidationPlugin.validateAsync} was called. * @param options The options passed to the {@link ValidationPlugin.validateAsync} method. @@ -229,11 +226,11 @@ export function validationPlugin( field.errorCount = 0; field.errorOrigin = 0; field.validator = typeof validator === 'function' ? { validate: validator } : validator; - field.validation = null; + field.validation = field.parent !== null ? field.parent.validation : null; Object.defineProperties(field, { - isInvalid: { get: () => field.errorCount !== 0 }, - isValidating: { get: () => field.validation !== null }, + isInvalid: { configurable: true, get: () => field.errorCount !== 0 }, + isValidating: { configurable: true, get: () => field.validation !== null }, }); const { setValue, setTransientValue } = field; @@ -279,7 +276,7 @@ export function validationPlugin( }; field.setValidationError = (validation, error) => { - if (validation !== null && field.validation !== validation && field.errorOrigin < 2) { + if (validation !== null && field.validation === validation && field.errorOrigin < 2) { dispatchEvents(setError(field, error, 1, [])); } }; diff --git a/packages/roqueform/src/test/composePlugins.test-d.ts b/packages/roqueform/src/test/composePlugins.test-d.ts index a771293d..df8ebc07 100644 --- a/packages/roqueform/src/test/composePlugins.test-d.ts +++ b/packages/roqueform/src/test/composePlugins.test-d.ts @@ -1,10 +1,10 @@ import { expectType } from 'tsd'; -import { composePlugins, createField, Plugin } from 'roqueform'; +import { composePlugins, createField, PluginInjector } from 'roqueform'; -declare const plugin1: Plugin<{ aaa: number }>; -declare const plugin2: Plugin<{ bbb: boolean }>; +declare const plugin1: PluginInjector<{ aaa: number }>; +declare const plugin2: PluginInjector<{ bbb: boolean }>; -expectType>(composePlugins(plugin1, plugin2)); +expectType>(composePlugins(plugin1, plugin2)); const field = createField({ foo: 111 }, composePlugins(plugin1, plugin2)); diff --git a/packages/roqueform/src/test/createField.test.ts b/packages/roqueform/src/test/createField.test.ts index 0c65157d..ef0ead25 100644 --- a/packages/roqueform/src/test/createField.test.ts +++ b/packages/roqueform/src/test/createField.test.ts @@ -1,4 +1,4 @@ -import { createField, naturalAccessor, Plugin } from '../main'; +import { createField, naturalAccessor } from '../main'; jest.useFakeTimers(); @@ -10,103 +10,172 @@ describe('createField', () => { test('creates a field without an initial value', () => { const field = createField(); - expect(field.parent).toBe(null); - expect(field.key).toBe(null); - expect(field.value).toBe(undefined); + expect(field.key).toBeNull(); + expect(field.value).toBeUndefined(); + expect(field.initialValue).toBeUndefined(); expect(field.isTransient).toBe(false); + expect(field.root).toBe(field); + expect(field.parent).toBeNull(); + expect(field.children).toBeNull(); + expect(field.childrenMap).toBeNull(); + expect(field.subscribers).toBeNull(); + expect(field.valueAccessor).toBe(naturalAccessor); + expect(field.plugin).toBeNull(); }); test('creates a field with the initial value', () => { const field = createField(111); expect(field.value).toBe(111); + expect(field.initialValue).toBe(111); }); test('returns a field at key', () => { - const field = createField({ foo: 111 }); + const field = createField({ aaa: 111 }); - expect(field.at('foo').parent).toBe(field); - expect(field.at('foo').key).toBe('foo'); - expect(field.at('foo').value).toBe(111); + const child = field.at('aaa'); + + expect(child.root).toBe(field); + expect(child.parent).toBe(field); + expect(child.key).toBe('aaa'); + expect(child.value).toBe(111); + expect(child.initialValue).toBe(111); + + expect(field.children).toEqual([child]); + expect(field.childrenMap).toEqual(new Map([['aaa', child]])); }); test('returns the same field for the same key', () => { - const field = createField({ foo: 111 }); + const field = createField({ aaa: 111 }); - expect(field.at('foo')).toBe(field.at('foo')); + expect(field.at('aaa')).toBe(field.at('aaa')); }); - test('dispatches a value to a root field', () => { + test('sets a value to a root field', () => { const field = createField(111); field.setValue(222); expect(field.value).toBe(222); + expect(field.initialValue).toBe(111); expect(field.isTransient).toBe(false); }); - test('dispatches a value to a derived field', () => { - const field = createField({ foo: 111 }); + test('sets a value to a child field', () => { + const initialValue = { aaa: 111 }; + + const field = createField(initialValue); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); - expect(field.value).toEqual({ foo: 222 }); + expect(field.value).not.toBe(initialValue); + expect(field.value).toEqual({ aaa: 222 }); + expect(field.initialValue).toEqual(initialValue); expect(field.isTransient).toBe(false); - expect(field.at('foo').value).toBe(222); - expect(field.at('foo').isTransient).toBe(false); + expect(field.at('aaa').value).toBe(222); + expect(field.at('aaa').initialValue).toBe(111); + expect(field.at('aaa').isTransient).toBe(false); }); - test('invokes a subscriber when value is updated', () => { + test('calls a glob subscriber when value is updated', () => { + const rootSubscriberMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); + + const field = createField({ aaa: 111 }); + field.on('*', rootSubscriberMock); + + field.at('aaa').on('*', aaaSubscriberMock); + + field.at('aaa').setValue(222); + + expect(rootSubscriberMock).toHaveBeenCalledTimes(1); + expect(rootSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field.at('aaa'), + target: field, + data: { aaa: 111 }, + }); + + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field.at('aaa'), + target: field.at('aaa'), + data: 111, + }); + }); + + test('calls a type subscriber when value is updated', () => { const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - const field = createField({ foo: 111 }); - field.subscribe(subscriberMock); + const field = createField({ aaa: 111 }); + field.on('change:value', subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.at('aaa').on('change:value', aaaSubscriberMock); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(subscriberMock).toHaveBeenNthCalledWith(1, field.at('foo'), field); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field.at('aaa'), + target: field, + data: { aaa: 111 }, + }); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenNthCalledWith(1, field.at('foo'), field.at('foo')); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field.at('aaa'), + target: field.at('aaa'), + data: 111, + }); }); test('does not invoke the subscriber of the unchanged sibling field', () => { - const fooListenerMock = jest.fn(); - const barListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); + const bbbSubscriberMock = jest.fn(); - const field = createField({ foo: 111, bar: 'aaa' }); + const field = createField({ aaa: 111, bbb: 'aaa' }); - field.at('foo').subscribe(fooListenerMock); - field.at('bar').subscribe(barListenerMock); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('bbb').on('*', bbbSubscriberMock); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); - expect(barListenerMock).not.toHaveBeenCalled(); + expect(bbbSubscriberMock).not.toHaveBeenCalled(); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenNthCalledWith(1, field.at('foo'), field.at('foo')); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field.at('aaa'), + target: field.at('aaa'), + data: 111, + }); }); - test('does not invoke the subscriber of the unchanged derived field', () => { - const fooListenerMock = jest.fn(); - const barListenerMock = jest.fn(); + test('does not invoke the subscriber of the unchanged child field', () => { + const aaaSubscriberMock = jest.fn(); + const bbbSubscriberMock = jest.fn(); - const field = createField({ foo: 111, bar: 'aaa' }); + const field = createField({ aaa: 111, bbb: 'aaa' }); - field.at('foo').subscribe(fooListenerMock); - field.at('bar').subscribe(barListenerMock); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('bbb').on('*', bbbSubscriberMock); - field.setValue({ foo: 222, bar: 'aaa' }); + field.setValue({ aaa: 222, bbb: 'aaa' }); - expect(barListenerMock).not.toHaveBeenCalled(); + expect(bbbSubscriberMock).not.toHaveBeenCalled(); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenNthCalledWith(1, field, field.at('foo')); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field, + target: field.at('aaa'), + data: 111, + }); }); test('sets a value to a root field', () => { @@ -118,155 +187,141 @@ describe('createField', () => { expect(field.isTransient).toBe(true); }); - test('sets a value to a derived field', () => { - const initialValue = { foo: 111 }; + test('sets a value to a child field', () => { + const initialValue = { aaa: 111 }; const field = createField(initialValue); - field.at('foo').setTransientValue(222); + field.at('aaa').setTransientValue(222); expect(field.value).toBe(initialValue); expect(field.isTransient).toBe(false); - expect(field.at('foo').value).toBe(222); - expect(field.at('foo').isTransient).toBe(true); + expect(field.at('aaa').value).toBe(222); + expect(field.at('aaa').isTransient).toBe(true); }); - test('dispatches a value after it was set to a derived field', () => { - const field = createField({ foo: 111 }); + test('propagates a value after it was set to a child field', () => { + const field = createField({ aaa: 111 }); - field.at('foo').setTransientValue(222); - field.at('foo').dispatch(); + field.at('aaa').setTransientValue(222); + field.at('aaa').propagate(); - expect(field.value).toEqual({ foo: 222 }); + expect(field.value).toEqual({ aaa: 222 }); expect(field.isTransient).toBe(false); - expect(field.at('foo').value).toBe(222); - expect(field.at('foo').isTransient).toBe(false); + expect(field.at('aaa').value).toBe(222); + expect(field.at('aaa').isTransient).toBe(false); }); test('invokes a subscriber when a value is updated transiently', () => { const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - const field = createField({ foo: 111 }); - field.subscribe(subscriberMock); + const field = createField({ aaa: 111 }); + field.on('*', subscriberMock); - field.at('foo').subscribe(fooListenerMock); - field.at('foo').setTransientValue(222); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('aaa').setTransientValue(222); expect(subscriberMock).toHaveBeenCalledTimes(0); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenNthCalledWith(1, field.at('foo'), field.at('foo')); - }); - - test('does not leave the form in an inconsistent state if a subscriber throws an error', () => { - const fooListenerMock = jest.fn(() => { - throw new Error('fooExpected'); - }); - const barListenerMock = jest.fn(() => { - throw new Error('barExpected'); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field.at('aaa'), + target: field.at('aaa'), + data: 111, }); - - const field = createField({ foo: 111, bar: 222 }); - - field.at('foo').subscribe(fooListenerMock); - field.at('bar').subscribe(barListenerMock); - - field.setValue({ foo: 333, bar: 444 }); - - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(barListenerMock).toHaveBeenCalledTimes(1); - expect(field.at('foo').value).toBe(333); - expect(field.at('bar').value).toBe(444); - - expect(() => jest.runAllTimers()).toThrow(new Error('fooExpected')); - expect(() => jest.runAllTimers()).toThrow(new Error('barExpected')); }); - test('calls all subscribers and throws error asynchronously', () => { - const subscriberMock1 = jest.fn(() => { - throw new Error('expected1'); + test('does not leave fields in an inconsistent state if a subscriber throws an error', () => { + const aaaSubscriberMock = jest.fn(() => { + throw new Error('aaaExpected'); }); - const subscriberMock2 = jest.fn(() => { - throw new Error('expected2'); + const bbbSubscriberMock = jest.fn(() => { + throw new Error('bbbExpected'); }); - const field = createField({ foo: 111, bar: 222 }); - - field.at('foo').subscribe(subscriberMock1); - field.at('foo').subscribe(subscriberMock2); + const field = createField({ aaa: 111, bbb: 222 }); - field.setValue({ foo: 333, bar: 444 }); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('bbb').on('*', bbbSubscriberMock); - expect(subscriberMock1).toHaveBeenCalledTimes(1); - expect(subscriberMock2).toHaveBeenCalledTimes(1); - expect(field.at('foo').value).toBe(333); - expect(field.at('bar').value).toBe(444); + try { + field.setValue({ aaa: 333, bbb: 444 }); + } catch {} - expect(() => jest.runAllTimers()).toThrow(new Error('expected1')); - expect(() => jest.runAllTimers()).toThrow(new Error('expected2')); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(bbbSubscriberMock).toHaveBeenCalledTimes(0); + expect(field.at('aaa').value).toBe(333); + expect(field.at('bbb').value).toBe(444); }); - test('propagates a new value to the derived field', () => { + test('propagates a new value to the child field', () => { const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - const field = createField({ foo: 111 }); - field.subscribe(subscriberMock); + const field = createField({ aaa: 111 }); + field.on('*', subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.at('aaa').on('*', aaaSubscriberMock); - const nextValue = { foo: 333 }; + const nextValue = { aaa: 333 }; field.setValue(nextValue); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(field.value).toBe(nextValue); expect(field.isTransient).toBe(false); - expect(field.at('foo').value).toBe(333); - expect(field.at('foo').isTransient).toBe(false); + expect(field.at('aaa').value).toBe(333); + expect(field.at('aaa').isTransient).toBe(false); }); - test('does not propagate a new value to the transient derived field', () => { + test('does not propagate a new value to the transient child field', () => { const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - const field = createField({ foo: 111 }); - field.subscribe(subscriberMock); + const field = createField({ aaa: 111 }); + field.on('*', subscriberMock); - field.at('foo').subscribe(fooListenerMock); - field.at('foo').setTransientValue(222); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('aaa').setTransientValue(222); - field.setValue({ foo: 333 }); + field.setValue({ aaa: 333 }); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); - expect(field.at('foo').value).toBe(222); - expect(field.at('foo').isTransient).toBe(true); + expect(field.at('aaa').value).toBe(222); + expect(field.at('aaa').isTransient).toBe(true); }); - test('does not notify subscribers if a value of the derived field did not change', () => { + test('does not notify subscribers if a value of the child field did not change', () => { const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - const fooValue = { bar: 111 }; + const aaaValue = { bbb: 111 }; + const initialValue = { aaa: aaaValue }; - const field = createField({ foo: fooValue }); - field.subscribe(subscriberMock); + const field = createField(initialValue); + field.on('*', subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.at('aaa').on('*', aaaSubscriberMock); - field.setValue({ foo: fooValue }); + field.setValue({ aaa: aaaValue }); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(subscriberMock).toHaveBeenNthCalledWith(1, field, field); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:value', + origin: field, + target: field, + data: initialValue, + }); - expect(fooListenerMock).toHaveBeenCalledTimes(0); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(0); }); test('applies a plugin to the root field', () => { @@ -275,96 +330,34 @@ describe('createField', () => { const field = createField(111, pluginMock); expect(pluginMock).toHaveBeenCalledTimes(1); - expect(pluginMock).toHaveBeenNthCalledWith(1, field, naturalAccessor, expect.any(Function)); + expect(pluginMock).toHaveBeenNthCalledWith(1, field); }); - test('returns a field if plugin returns undefined', () => { + test('applies a plugin to the child field', () => { const pluginMock = jest.fn(); - const field = createField(111, pluginMock); - - expect(field.value).toBe(111); + const field = createField({ aaa: 111 }, pluginMock); - expect(pluginMock).toHaveBeenCalledTimes(1); - expect(pluginMock).toHaveBeenNthCalledWith(1, field, naturalAccessor, expect.any(Function)); - }); - - test('applies a plugin to the derived field', () => { - const pluginMock = jest.fn(); - - const field = createField({ foo: 111 }, pluginMock); - - const fooField = field.at('foo'); + const aaaField = field.at('aaa'); expect(pluginMock).toHaveBeenCalledTimes(2); - expect(pluginMock).toHaveBeenNthCalledWith(1, field, naturalAccessor, expect.any(Function)); - expect(pluginMock).toHaveBeenNthCalledWith(2, fooField, naturalAccessor, expect.any(Function)); - }); - - test('plugin notifies field subscribers', () => { - let notifyCallback1!: () => void; - - const plugin: Plugin = jest.fn().mockImplementationOnce((_field, _accessor, notify) => { - notifyCallback1 = notify; - }); - const subscriberMock1 = jest.fn(); - const subscriberMock2 = jest.fn(); - - const field = createField({ foo: 111 }, plugin); - - field.subscribe(subscriberMock1); - field.at('foo').subscribe(subscriberMock2); - - expect(subscriberMock1).not.toHaveBeenCalled(); - expect(subscriberMock2).not.toHaveBeenCalled(); - - notifyCallback1(); - - expect(subscriberMock1).toHaveBeenCalledTimes(1); - expect(subscriberMock2).not.toHaveBeenCalled(); - }); - - test('plugin notifies derived field subscribers', () => { - let notifyCallback1!: () => void; - - const plugin: Plugin = jest - .fn() - .mockImplementationOnce(() => undefined) - .mockImplementationOnce((_field, _accessor, notify) => { - notifyCallback1 = notify; - }); - - const subscriberMock1 = jest.fn(); - const subscriberMock2 = jest.fn(); - - const field = createField({ foo: 111 }, plugin); - - field.subscribe(subscriberMock1); - field.at('foo').subscribe(subscriberMock2); - - expect(subscriberMock1).not.toHaveBeenCalled(); - expect(subscriberMock2).not.toHaveBeenCalled(); - - notifyCallback1(); - - expect(subscriberMock1).not.toHaveBeenCalled(); - expect(subscriberMock2).toHaveBeenCalledTimes(1); + expect(pluginMock).toHaveBeenNthCalledWith(1, field); + expect(pluginMock).toHaveBeenNthCalledWith(2, aaaField); }); - test('an actual parent value is visible in the derived field subscriber', done => { - const field = createField({ foo: 111 }); - const newValue = { foo: 222 }; + test('an actual parent value is visible in the child field subscriber', done => { + const field = createField({ aaa: 111 }); + const newValue = { aaa: 222 }; - field.at('foo').subscribe(updatedField => { - expect(updatedField).toBe(field); - expect(updatedField.value).toBe(newValue); + field.at('aaa').on('*', event => { + expect(event.origin.value).toBe(newValue); done(); }); field.setValue(newValue); }); - test('does not cache a derived field for which the plugin has thrown an error', () => { + test('does not cache a child field for which the plugin has thrown an error', () => { const pluginMock = jest.fn(); pluginMock.mockImplementationOnce(() => undefined); pluginMock.mockImplementationOnce(() => { @@ -374,10 +367,10 @@ describe('createField', () => { throw new Error('expected2'); }); - const field = createField({ foo: 111 }, pluginMock); + const field = createField({ aaa: 111 }, pluginMock); - expect(() => field.at('foo')).toThrow(new Error('expected1')); - expect(() => field.at('foo')).toThrow(new Error('expected2')); + expect(() => field.at('aaa')).toThrow(new Error('expected1')); + expect(() => field.at('aaa')).toThrow(new Error('expected2')); }); test('setting field value in a subscriber does not trigger an infinite loop', () => { @@ -387,7 +380,7 @@ describe('createField', () => { field.setValue(333); }); - field.subscribe(subscriberMock); + field.on('*', subscriberMock); field.setValue(222); @@ -395,18 +388,18 @@ describe('createField', () => { expect(subscriberMock).toHaveBeenCalledTimes(2); }); - test('setting field value in a derived field subscriber does not trigger an infinite loop', () => { - const field = createField({ foo: 111 }); + test('setting field value in a child field subscriber does not trigger an infinite loop', () => { + const field = createField({ aaa: 111 }); const subscriberMock = jest.fn(() => { - field.at('foo').setValue(333); + field.at('aaa').setValue(333); }); - field.subscribe(subscriberMock); + field.on('*', subscriberMock); - field.at('foo').setValue(222); + field.at('aaa').setValue(222); - expect(field.value.foo).toBe(333); + expect(field.value.aaa).toBe(333); expect(subscriberMock).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/roqueform/src/test/naturalAccessor.test.ts b/packages/roqueform/src/test/naturalAccessor.test.ts index cf32b545..de4dc33a 100644 --- a/packages/roqueform/src/test/naturalAccessor.test.ts +++ b/packages/roqueform/src/test/naturalAccessor.test.ts @@ -2,14 +2,14 @@ import { naturalAccessor } from '../main'; describe('naturalAccessor', () => { test('does not read value from primitive values', () => { - expect(naturalAccessor.get(null, 'aaa')).toBe(undefined); - expect(naturalAccessor.get(undefined, 'aaa')).toBe(undefined); - expect(naturalAccessor.get(111, 'toString')).toBe(undefined); - expect(naturalAccessor.get('aaa', 'toString')).toBe(undefined); - expect(naturalAccessor.get(true, 'toString')).toBe(undefined); - expect(naturalAccessor.get(() => undefined, 'length')).toBe(undefined); - expect(naturalAccessor.get(new Date(), 'now')).toBe(undefined); - expect(naturalAccessor.get(new RegExp(''), 'lastIndex')).toBe(undefined); + expect(naturalAccessor.get(null, 'aaa')).toBeUndefined(); + expect(naturalAccessor.get(undefined, 'aaa')).toBeUndefined(); + expect(naturalAccessor.get(111, 'toString')).toBeUndefined(); + expect(naturalAccessor.get('aaa', 'toString')).toBeUndefined(); + expect(naturalAccessor.get(true, 'toString')).toBeUndefined(); + expect(naturalAccessor.get(() => undefined, 'length')).toBeUndefined(); + expect(naturalAccessor.get(new Date(), 'now')).toBeUndefined(); + expect(naturalAccessor.get(new RegExp(''), 'lastIndex')).toBeUndefined(); }); test('reads value from an array', () => { @@ -106,6 +106,6 @@ describe('naturalAccessor', () => { expect(result).toEqual({ aaa: 111, bbb: 222 }); expect(result).not.toBe(obj); - expect(Object.getPrototypeOf(result)).toBe(null); + expect(Object.getPrototypeOf(result)).toBeNull(); }); }); diff --git a/packages/roqueform/src/test/utils.test.ts b/packages/roqueform/src/test/utils.test.ts index 78bd32b3..bce76d86 100644 --- a/packages/roqueform/src/test/utils.test.ts +++ b/packages/roqueform/src/test/utils.test.ts @@ -1,64 +1,19 @@ -import { callAll, callOrGet } from '../main'; - -jest.useFakeTimers(); +import { callOrGet } from '../main'; describe('callOrGet', () => { test('returns non function value as is', () => { const obj = {}; - expect(callOrGet(123)).toBe(123); - expect(callOrGet(null)).toBe(null); - expect(callOrGet(obj)).toBe(obj); + expect(callOrGet(111, undefined)).toBe(111); + expect(callOrGet(null, undefined)).toBeNull(); + expect(callOrGet(obj, undefined)).toBe(obj); }); test('returns the function call result', () => { - expect(callOrGet(() => 123)).toBe(123); + expect(callOrGet(() => 111, undefined)).toBe(111); }); test('passes arguments to a function', () => { - expect(callOrGet((arg1, arg2) => arg1 + arg2, [123, 456])).toBe(579); - }); -}); - -describe('callAll', () => { - test('calls all callbacks with the same set of arguments', () => { - const cbMock1 = jest.fn(); - const cbMock2 = jest.fn(); - const cbMock3 = jest.fn(); - - callAll([cbMock1, cbMock2, cbMock3], ['foo', 'bar']); - - expect(cbMock1).toHaveBeenCalledTimes(1); - expect(cbMock2).toHaveBeenCalledTimes(1); - expect(cbMock3).toHaveBeenCalledTimes(1); - - expect(cbMock1).toHaveBeenNthCalledWith(1, 'foo', 'bar'); - expect(cbMock2).toHaveBeenNthCalledWith(1, 'foo', 'bar'); - expect(cbMock3).toHaveBeenNthCalledWith(1, 'foo', 'bar'); - }); - - test('throws errors asynchronously', () => { - const cbMock1 = jest.fn(() => {}); - const cbMock2 = jest.fn(() => { - throw new Error('expected2'); - }); - const cbMock3 = jest.fn(() => { - throw new Error('expected3'); - }); - - callAll([cbMock1, cbMock2, cbMock3]); - - expect(cbMock1).toHaveBeenCalledTimes(1); - expect(cbMock2).toHaveBeenCalledTimes(1); - expect(cbMock3).toHaveBeenCalledTimes(1); - - expect(() => jest.runAllTimers()).toThrow(); - }); - - test('does not call the same callback twice', () => { - const cbMock1 = jest.fn(() => {}); - - callAll([cbMock1, cbMock1]); - expect(cbMock1).toHaveBeenCalledTimes(1); + expect(callOrGet(arg => arg, 111)).toBe(111); }); }); diff --git a/packages/roqueform/src/test/validationPlugin.test-d.ts b/packages/roqueform/src/test/validationPlugin.test-d.ts index 10e714b0..469f841d 100644 --- a/packages/roqueform/src/test/validationPlugin.test-d.ts +++ b/packages/roqueform/src/test/validationPlugin.test-d.ts @@ -1,4 +1,4 @@ import { expectType } from 'tsd'; -import { Plugin, ValidationPlugin, validationPlugin } from 'roqueform'; +import { PluginInjector, ValidationPlugin, validationPlugin } from 'roqueform'; -expectType>>(validationPlugin(() => undefined)); +expectType>>(validationPlugin(() => undefined)); diff --git a/packages/roqueform/src/test/validationPlugin.test.tsx b/packages/roqueform/src/test/validationPlugin.test.tsx index 70baae65..691a7502 100644 --- a/packages/roqueform/src/test/validationPlugin.test.tsx +++ b/packages/roqueform/src/test/validationPlugin.test.tsx @@ -6,297 +6,334 @@ describe('validationPlugin', () => { }; test('enhances the field', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); - expect(field.isValidating).toBe(false); - expect(field.isInvalid).toBe(false); expect(field.error).toBe(null); - - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.isInvalid).toBe(false); + expect(field.isValidating).toBe(false); + expect(field.errorCount).toBe(0); + expect(field.errorOrigin).toBe(0); + expect(field.validator).toBe(noopValidator); + expect(field.validation).toBeNull(); + + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('sets an error to the root field', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - field.setError(111); + field.setError(222); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(111); + expect(field.error).toBe(222); + expect(field.errorCount).toBe(1); + expect(field.errorOrigin).toBe(2); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).not.toHaveBeenCalled(); + expect(aaaSubscriberMock).not.toHaveBeenCalled(); }); test('sets an error to the child field', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - field.at('foo').setError(111); + field.at('aaa').setError(222); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); + expect(field.errorCount).toBe(1); + expect(field.errorOrigin).toBe(0); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(222); + expect(field.at('aaa').errorCount).toBe(1); + expect(field.at('aaa').errorOrigin).toBe(2); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); }); - test('sets null as an error to the root field', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + test('deletes an error if null is set', () => { + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); + field.setError(222); field.setError(null); - expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.isInvalid).toBe(false); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('deletes an error from the root field', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); field.setError(111); field.deleteError(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); expect(subscriberMock).toHaveBeenCalledTimes(2); - expect(fooListenerMock).not.toHaveBeenCalled(); + expect(aaaSubscriberMock).not.toHaveBeenCalled(); }); test('deletes an error from the child field', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - field.at('foo').setError(111); - field.at('foo').deleteError(); + field.at('aaa').setError(222); + field.at('aaa').deleteError(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); expect(subscriberMock).toHaveBeenCalledTimes(2); - expect(fooListenerMock).toHaveBeenCalledTimes(2); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(2); }); test('deletes an error from the child field but parent remains invalid', () => { - const field = createField({ foo: 0, bar: 'qux' }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111, bbb: 222 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); - const barListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); + const bbbSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); - field.at('bar').subscribe(barListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('bbb').on('*', bbbSubscriberMock); - field.at('foo').setError(111); - field.at('bar').setError(222); + field.at('aaa').setError(333); + field.at('bbb').setError(444); - field.at('bar').deleteError(); + field.at('bbb').deleteError(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(333); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); - expect(barListenerMock).toHaveBeenCalledTimes(2); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); + expect(bbbSubscriberMock).toHaveBeenCalledTimes(2); }); test('clears all errors', () => { - const field = createField({ foo: 0, bar: 'qux' }, validationPlugin(noopValidator)); + const field = createField({ aaa: 111, bbb: 222 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); - const barListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); + const bbbSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); - field.at('bar').subscribe(barListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); + field.at('bbb').on('*', bbbSubscriberMock); - field.at('foo').setError(111); - field.at('bar').setError(222); + field.at('aaa').setError(333); + field.at('bbb').setError(444); field.clearErrors(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); expect(subscriberMock).toHaveBeenCalledTimes(2); - expect(fooListenerMock).toHaveBeenCalledTimes(2); - expect(barListenerMock).toHaveBeenCalledTimes(2); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(2); + expect(bbbSubscriberMock).toHaveBeenCalledTimes(2); }); test('clears errors from nested fields', () => { const field = createField( { - foo: { - bar: { - baz: 'aaa', - qux: 'bbb', + aaa: { + bbb: { + ccc: 111, + ddd: 222, }, }, }, validationPlugin(noopValidator) ); - field.at('foo').at('bar').at('baz').setError(111); - field.at('foo').at('bar').at('qux').setError(111); + field.at('aaa').at('bbb').at('ccc').setError(333); + field.at('aaa').at('bbb').at('ddd').setError(444); field.clearErrors(); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').at('bar').isInvalid).toBe(false); - expect(field.at('foo').at('bar').at('baz').isInvalid).toBe(false); - expect(field.at('foo').at('bar').at('qux').isInvalid).toBe(false); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').at('bbb').isInvalid).toBe(false); + expect(field.at('aaa').at('bbb').at('ccc').isInvalid).toBe(false); + expect(field.at('aaa').at('bbb').at('ddd').isInvalid).toBe(false); }); test('synchronously validates the root field', () => { const field = createField( - { foo: 0 }, + { aaa: 111 }, validationPlugin({ - validate(field, setError) { - setError(field.at('foo'), 111); + validate(field) { + field.at('aaa').setValidationError(field.validation!, 222); }, }) ); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - expect(field.validate()).toEqual([111]); + expect(field.validate()).toBe(false); expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(3); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'validation:start', + origin: field, + target: field, + data: undefined, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:error', + origin: field.at('aaa'), + target: field, + data: null, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(3, { + type: 'validation:end', + origin: field, + target: field, + data: undefined, + }); + + expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); test('synchronously validates the root field with a callback validator', () => { const field = createField( - { foo: 0 }, - validationPlugin((field, setError) => { - setError(field.at('foo'), 111); + { aaa: 111 }, + validationPlugin(field => { + field.at('aaa').setValidationError(field.validation!, 222); }) ); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - expect(field.validate()).toEqual([111]); + expect(field.validate()).toBe(false); expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(3); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); test('synchronously validates the child field', () => { const field = createField( - { foo: 0 }, + { aaa: 111 }, validationPlugin({ - validate(field, setError) { - setError(field, 111); + validate(field) { + field.setValidationError(field.validation!, 222); }, }) ); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - field.at('foo').validate(); + field.at('aaa').validate(); expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(222); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:error', + origin: field.at('aaa'), + target: field, + data: null, + }); + + expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); test('synchronously validates multiple fields', () => { const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ - validate(field, setError) { - setError(field.at('foo'), 111); - setError(field.at('bar'), 222); + validate(field) { + field.at('aaa').setValidationError(field.validation!, 333); + field.at('bbb').setValidationError(field.validation!, 444); }, }) ); @@ -304,29 +341,29 @@ describe('validationPlugin', () => { field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(333); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toBe(222); + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toBe(444); }); test('clears previous validation errors before validation', () => { const validateMock = jest.fn(); - validateMock.mockImplementationOnce((field, setError) => { - setError(field.at('foo'), 111); - setError(field.at('bar'), 222); + validateMock.mockImplementationOnce(field => { + field.at('aaa').setValidationError(field.validation, 111); + field.at('bbb').setValidationError(field.validation, 222); }); - validateMock.mockImplementationOnce((field, setError) => { - setError(field.at('foo'), 111); + validateMock.mockImplementationOnce(field => { + field.at('aaa').setValidationError(field.validation, 111); }); const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 0, bbb: 'ddd' }, validationPlugin({ validate: validateMock, }) @@ -338,143 +375,143 @@ describe('validationPlugin', () => { expect(validateMock).toHaveBeenCalledTimes(2); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(111); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); }); test('does not clear an error set by the user before validation', () => { const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 0, bbb: 'ddd' }, validationPlugin({ - validate(field, setError) { - setError(field.at('foo'), 111); + validate(field) { + field.at('aaa').setValidationError(field.validation!, 111); }, }) ); - field.at('bar').setError(222); + field.at('bbb').setError(222); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual(111); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual(111); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toBe(222); + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toBe(222); }); - test('does not raise validation errors for transient fields', () => { + test('does not set validation errors for transient fields', () => { const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 0, bbb: 'ddd' }, validationPlugin({ - validate(field, setError) { - setError(field.at('foo'), 111); - setError(field.at('bar'), 222); + validate(field) { + field.at('aaa').setValidationError(field.validation!, 111); + field.at('bbb').setValidationError(field.validation!, 222); }, }) ); - field.at('bar').setTransientValue(''); + field.at('bbb').setTransientValue(''); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(111); - expect(field.at('bar').isInvalid).toBe(false); - expect(field.at('bar').error).toBe(null); + expect(field.at('bbb').isInvalid).toBe(false); + expect(field.at('bbb').error).toBeNull(); }); test('asynchronously validates the root field', async () => { const field = createField( - { foo: 0 }, + { aaa: 111 }, validationPlugin({ validate: () => undefined, - async validateAsync(field, setError) { - setError(field.at('foo'), 111); + async validateAsync(field) { + field.at('aaa').setValidationError(field.validation!, 222); }, }) ); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); const promise = field.validateAsync(); expect(promise).toBeInstanceOf(Promise); expect(field.isValidating).toBe(true); - expect(field.at('foo').isValidating).toBe(true); + expect(field.at('aaa').isValidating).toBe(true); - await expect(promise).resolves.toEqual([111]); + await expect(promise).resolves.toEqual(false); expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(222); expect(subscriberMock).toHaveBeenCalledTimes(3); - expect(fooListenerMock).toHaveBeenCalledTimes(3); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); test('asynchronously validates the child field', async () => { const field = createField( - { foo: 0 }, + { aaa: 111 }, validationPlugin({ validate: () => undefined, - async validateAsync(field, setError) { - setError(field, 111); + async validateAsync(field) { + field.setValidationError(field.validation!, 222); }, }) ); const subscriberMock = jest.fn(); - const fooListenerMock = jest.fn(); + const aaaSubscriberMock = jest.fn(); - field.subscribe(subscriberMock); - field.at('foo').subscribe(fooListenerMock); + field.on('*', subscriberMock); + field.at('aaa').on('*', aaaSubscriberMock); - const promise = field.at('foo').validateAsync(); + const promise = field.at('aaa').validateAsync(); expect(field.isValidating).toBe(false); - expect(field.at('foo').isValidating).toBe(true); + expect(field.at('aaa').isValidating).toBe(true); - await expect(promise).resolves.toEqual([111]); + await expect(promise).resolves.toEqual(false); expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toBe(111); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toBe(222); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(fooListenerMock).toHaveBeenCalledTimes(3); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); test('cleans up validation if a sync error is thrown', () => { const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ validate() { throw new Error('expected'); @@ -482,22 +519,22 @@ describe('validationPlugin', () => { }) ); - field.at('foo'); + field.at('aaa'); expect(() => field.validate()).toThrow(new Error('expected')); expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('cleans up validation if an async error is thrown', async () => { const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ validate: () => undefined, @@ -507,7 +544,7 @@ describe('validationPlugin', () => { }) ); - field.at('foo'); + field.at('aaa'); const promise = field.validateAsync(); @@ -515,23 +552,23 @@ describe('validationPlugin', () => { expect(field.isValidating).toBe(false); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isValidating).toBe(false); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isValidating).toBe(false); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('aborts validation', async () => { let lastSignal: AbortSignal | undefined; const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ validate: () => undefined, - async validateAsync(_field, _setError, _context, signal) { - lastSignal = signal; + async validateAsync(field) { + lastSignal = field.validation!.abortController!.signal; }, }) ); @@ -539,27 +576,27 @@ describe('validationPlugin', () => { const promise = field.validateAsync(); expect(field.isValidating).toBe(true); - expect(field.at('foo').isValidating).toBe(true); + expect(field.at('aaa').isValidating).toBe(true); field.abortValidation(); expect(lastSignal!.aborted).toBe(true); expect(field.isValidating).toBe(false); - expect(field.at('foo').isValidating).toBe(false); + expect(field.at('aaa').isValidating).toBe(false); await expect(promise).rejects.toEqual(new Error('Validation aborted')); }); - test('validation aborts pending validation when invoked on the same field', async () => { + test('aborts pending validation when invoked on the same field', async () => { const signals: AbortSignal[] = []; const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ validate: () => undefined, - async validateAsync(_field, _setError, _context, signal) { - signals.push(signal); + async validateAsync(field) { + signals.push(field.validation!.abortController!.signal); }, }) ); @@ -574,40 +611,43 @@ describe('validationPlugin', () => { await expect(promise).rejects.toEqual(new Error('Validation aborted')); }); - test('derived field validation does not abort the parent validation', () => { + test('child field validation aborts the parent validation', async () => { const signals: AbortSignal[] = []; const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ validate: () => undefined, - async validateAsync(_field, _setError, _context, signal) { - signals.push(signal); + async validateAsync(field) { + signals.push(field.validation!.abortController!.signal); }, }) ); - field.validateAsync(); - field.at('foo').validateAsync(); + const promise = field.validateAsync(); + const aaaPromise = field.at('aaa').validateAsync(); - expect(signals[0].aborted).toBe(false); + await expect(promise).rejects.toEqual(new Error('Validation aborted')); + await expect(aaaPromise).resolves.toBe(true); + + expect(signals[0].aborted).toBe(true); expect(signals[1].aborted).toBe(false); }); test('does not apply errors from the aborted validation', async () => { const validateAsyncMock = jest.fn(); - validateAsyncMock.mockImplementationOnce((field, setError) => { - return Promise.resolve().then(() => setError(field.at('foo'), 111)); + validateAsyncMock.mockImplementationOnce(async field => { + field.at('aaa').setValidationError(field.validation, 333); }); - validateAsyncMock.mockImplementationOnce((field, setError) => { - return Promise.resolve().then(() => setError(field.at('bar'), 222)); + validateAsyncMock.mockImplementationOnce(async field => { + field.at('bbb').setValidationError(field.validation, 444); }); const field = createField( - { foo: 0, bar: 'qux' }, + { aaa: 111, bbb: 222 }, validationPlugin({ validate: () => undefined, validateAsync: validateAsyncMock, @@ -618,22 +658,22 @@ describe('validationPlugin', () => { await field.validateAsync(); - expect(field.at('foo').error).toBe(null); - expect(field.at('bar').error).toBe(222); + expect(field.at('aaa').error).toBeNull(); + expect(field.at('bbb').error).toBe(444); await expect(promise).rejects.toEqual(new Error('Validation aborted')); }); - test('validation can be called in subscribe', () => { - const field = createField({ foo: 0 }, validationPlugin(noopValidator)); + test('validation can be called in value change subscriber', () => { + const field = createField({ aaa: 111 }, validationPlugin(noopValidator)); const subscriberMock = jest.fn(() => { field.validate(); }); - field.subscribe(subscriberMock); + field.on('change:value', subscriberMock); - field.at('foo').setValue(111); + field.at('aaa').setValue(222); expect(subscriberMock).toHaveBeenCalledTimes(1); }); diff --git a/packages/scroll-to-error-plugin/README.md b/packages/scroll-to-error-plugin/README.md index b6825135..cef1f233 100644 --- a/packages/scroll-to-error-plugin/README.md +++ b/packages/scroll-to-error-plugin/README.md @@ -43,13 +43,13 @@ export const App = () => { event.preventDefault(); if (planetField.validate()) { - // Scroll to the error that is closest to the top left conrner of the document + // The valid form value to submit. + planetField.value; + } else { + // Errors are associated with fields automatically. + // Scroll to the error that is closest to the top left conrner of the document. planetField.scrollToError(0, { behavior: 'smooth' }); - return; } - - // The form value to submit - planetField.value; }; return ( @@ -59,7 +59,7 @@ export const App = () => { {nameField => ( <> { diff --git a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx index a5e801c0..1a40e429 100644 --- a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx +++ b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx @@ -26,7 +26,7 @@ class DOMRect { describe('scrollToErrorPlugin', () => { test('returns false if there are no errors', () => { const field = createField( - { foo: 111 }, + { aaa: 111 }, composePlugins( validationPlugin(() => undefined), scrollToErrorPlugin() @@ -38,118 +38,118 @@ describe('scrollToErrorPlugin', () => { test('scrolls to error at index with RTL text direction', async () => { const rootField = createField( - { foo: 111, bar: 'aaa' }, + { aaa: 111, bbb: 222 }, composePlugins( validationPlugin(() => undefined), scrollToErrorPlugin() ) ); - const fooElement = document.body.appendChild(document.createElement('input')); - const barElement = document.body.appendChild(document.createElement('input')); + const aaaElement = document.body.appendChild(document.createElement('input')); + const bbbElement = document.body.appendChild(document.createElement('input')); - rootField.at('foo').ref(fooElement); - rootField.at('bar').ref(barElement); + rootField.at('aaa').ref(aaaElement); + rootField.at('bbb').ref(bbbElement); - jest.spyOn(fooElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(100)); - jest.spyOn(barElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(200)); + jest.spyOn(aaaElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(100)); + jest.spyOn(bbbElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(200)); - const fooScrollIntoViewMock = (fooElement.scrollIntoView = jest.fn()); - const barScrollIntoViewMock = (barElement.scrollIntoView = jest.fn()); + const aaaScrollIntoViewMock = (aaaElement.scrollIntoView = jest.fn()); + const bbbScrollIntoViewMock = (bbbElement.scrollIntoView = jest.fn()); await act(() => { - rootField.at('foo').setError('error1'); - rootField.at('bar').setError('error2'); + rootField.at('aaa').setError('error1'); + rootField.at('bbb').setError('error2'); }); // Scroll to default index rootField.scrollToError(); - expect(fooScrollIntoViewMock).toHaveBeenCalledTimes(1); - expect(barScrollIntoViewMock).not.toHaveBeenCalled(); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); + expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to 0 rootField.scrollToError(0); - expect(fooScrollIntoViewMock).toHaveBeenCalledTimes(1); - expect(barScrollIntoViewMock).not.toHaveBeenCalled(); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); + expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to 1 rootField.scrollToError(1); - expect(fooScrollIntoViewMock).not.toHaveBeenCalled(); - expect(barScrollIntoViewMock).toHaveBeenCalledTimes(1); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); + expect(bbbScrollIntoViewMock).toHaveBeenCalledTimes(1); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to 2 rootField.scrollToError(2); - expect(fooScrollIntoViewMock).not.toHaveBeenCalled(); - expect(barScrollIntoViewMock).not.toHaveBeenCalled(); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); + expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to -1 rootField.scrollToError(1); - expect(fooScrollIntoViewMock).not.toHaveBeenCalled(); - expect(barScrollIntoViewMock).toHaveBeenCalledTimes(1); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); + expect(bbbScrollIntoViewMock).toHaveBeenCalledTimes(1); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to -2 rootField.scrollToError(-2); - expect(fooScrollIntoViewMock).toHaveBeenCalledTimes(1); - expect(barScrollIntoViewMock).not.toHaveBeenCalled(); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); + expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to -3 rootField.scrollToError(-3); - expect(fooScrollIntoViewMock).not.toHaveBeenCalled(); - expect(barScrollIntoViewMock).not.toHaveBeenCalled(); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); + expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); }); test('scrolls to error at index with LTR text direction', async () => { const rootField = createField( - { foo: 111, bar: 'aaa' }, + { aaa: 111, bbb: 222 }, composePlugins( validationPlugin(() => undefined), scrollToErrorPlugin() ) ); - const fooElement = document.body.appendChild(document.createElement('input')); - const barElement = document.body.appendChild(document.createElement('input')); + const aaaElement = document.body.appendChild(document.createElement('input')); + const bbbElement = document.body.appendChild(document.createElement('input')); - rootField.at('foo').ref(fooElement); - rootField.at('bar').ref(barElement); + rootField.at('aaa').ref(aaaElement); + rootField.at('bbb').ref(bbbElement); - jest.spyOn(fooElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(100)); - jest.spyOn(barElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(200)); + jest.spyOn(aaaElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(100)); + jest.spyOn(bbbElement, 'getBoundingClientRect').mockImplementation(() => new DOMRect(200)); - const fooScrollIntoViewMock = (fooElement.scrollIntoView = jest.fn()); - const barScrollIntoViewMock = (barElement.scrollIntoView = jest.fn()); + const aaaScrollIntoViewMock = (aaaElement.scrollIntoView = jest.fn()); + const bbbScrollIntoViewMock = (bbbElement.scrollIntoView = jest.fn()); await act(() => { - rootField.at('foo').setError('error1'); - rootField.at('bar').setError('error2'); + rootField.at('aaa').setError('error1'); + rootField.at('bbb').setError('error2'); }); // Scroll to 0 rootField.scrollToError(0, { direction: 'ltr' }); - expect(fooScrollIntoViewMock).not.toHaveBeenCalled(); - expect(barScrollIntoViewMock).toHaveBeenCalledTimes(1); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); + expect(bbbScrollIntoViewMock).toHaveBeenCalledTimes(1); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); // Scroll to 1 rootField.scrollToError(1, { direction: 'ltr' }); - expect(fooScrollIntoViewMock).toHaveBeenCalledTimes(1); - expect(barScrollIntoViewMock).not.toHaveBeenCalled(); - fooScrollIntoViewMock.mockClear(); - barScrollIntoViewMock.mockClear(); + expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); + expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); + aaaScrollIntoViewMock.mockClear(); + bbbScrollIntoViewMock.mockClear(); }); }); diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index c5e735ea..347e7131 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -1,4 +1,4 @@ -import { dispatchEvents, Event as Event_, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; +import { dispatchEvents, Event, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; @@ -15,7 +15,8 @@ const elementValueAccessor = createElementValueAccessor(); export interface UncontrolledPlugin { /** * The array of elements that are used to derive the field value. Update this array by calling {@link observe} method. - * Elements are observed by the {@link MutationObserver} and deleted from this array when they are removed from DOM. + * Elements are observed by the {@link !MutationObserver MutationObserver} and deleted from this array when they are + * removed from DOM. * * @protected */ @@ -72,7 +73,7 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec field.elementValueAccessor = accessor; const mutationObserver = new MutationObserver(mutations => { - const events: Event_[] = []; + const events: Event[] = []; const { observedElements } = field; const [element] = observedElements; @@ -83,7 +84,9 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec if (elementIndex === -1) { continue; } + const element = observedElements[elementIndex]; + element.removeEventListener('input', changeListener); element.removeEventListener('change', changeListener); @@ -102,7 +105,7 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec dispatchEvents(events); }); - const changeListener = (event: Event): void => { + const changeListener: EventListener = event => { let value; if ( field.observedElements.indexOf(event.target as Element) !== -1 && diff --git a/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts b/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts index 80509c50..fee6be37 100644 --- a/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts +++ b/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts @@ -97,7 +97,7 @@ describe('createElementValueAccessor', () => { const accessor = createElementValueAccessor({ checkboxFormat: 'value' }); expect(accessor.get([createElement('input', { type: 'checkbox', checked: true, value: 'aaa' })])).toBe('aaa'); - expect(accessor.get([createElement('input', { type: 'checkbox', checked: false, value: 'aaa' })])).toBe(null); + expect(accessor.get([createElement('input', { type: 'checkbox', checked: false, value: 'aaa' })])).toBeNull(); }); test('returns an array of values for multiple checkboxes for "value" format', () => { @@ -149,7 +149,7 @@ describe('createElementValueAccessor', () => { }); test('returns null for empty number inputs', () => { - expect(accessor.get([createElement('input', { type: 'number' })])).toBe(null); + expect(accessor.get([createElement('input', { type: 'number' })])).toBeNull(); }); test('returns default value for empty range inputs', () => { @@ -167,11 +167,11 @@ describe('createElementValueAccessor', () => { }); test('returns null for empty date inputs by default', () => { - expect(accessor.get([createElement('input', { type: 'date' })])).toBe(null); + expect(accessor.get([createElement('input', { type: 'date' })])).toBeNull(); }); test('returns null for empty datetime-local inputs by default', () => { - expect(accessor.get([createElement('input', { type: 'date' })])).toBe(null); + expect(accessor.get([createElement('input', { type: 'date' })])).toBeNull(); }); test('returns input value for date inputs for "value" format', () => { @@ -235,7 +235,7 @@ describe('createElementValueAccessor', () => { }); test('returns null for empty time inputs by default', () => { - expect(accessor.get([createElement('input', { type: 'time' })])).toBe(null); + expect(accessor.get([createElement('input', { type: 'time' })])).toBeNull(); }); test('returns value for time inputs by default', () => { @@ -259,7 +259,7 @@ describe('createElementValueAccessor', () => { }); test('returns null for empty file inputs', () => { - expect(accessor.get([createElement('input', { type: 'file' })])).toBe(null); + expect(accessor.get([createElement('input', { type: 'file' })])).toBeNull(); }); test('returns an array for empty multi-file inputs', () => { @@ -281,7 +281,7 @@ describe('createElementValueAccessor', () => { accessor.set([element], 'aaa'); - expect(element.value).toBe(undefined); + expect(element.value).toBeUndefined(); }); test('sets checkboxes checked state from an array of booleans', () => { diff --git a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts index 5620e377..a5341703 100644 --- a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts +++ b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts @@ -1,5 +1,5 @@ import { ElementValueAccessor, uncontrolledPlugin } from '../main'; -import { composePlugins, createField, naturalAccessor } from 'roqueform'; +import { composePlugins, createField } from 'roqueform'; import { fireEvent } from '@testing-library/dom'; describe('uncontrolledPlugin', () => { @@ -15,34 +15,34 @@ describe('uncontrolledPlugin', () => { test('updates field value on input change', () => { const subscriberMock = jest.fn(); - const field = createField({ foo: 0 }, uncontrolledPlugin()); + const field = createField({ aaa: 111 }, uncontrolledPlugin()); element.type = 'number'; - field.subscribe(subscriberMock); - field.at('foo').ref(element); + field.on('*', subscriberMock); + field.at('aaa').observe(element); - fireEvent.change(element, { target: { value: '111' } }); + fireEvent.change(element, { target: { value: '222' } }); expect(subscriberMock).toHaveBeenCalledTimes(1); - expect(field.value).toEqual({ foo: 111 }); + expect(field.value).toEqual({ aaa: 222 }); }); test('updates input value on field change', () => { - const field = createField({ foo: 0 }, uncontrolledPlugin()); + const field = createField({ aaa: 111 }, uncontrolledPlugin()); - field.at('foo').ref(element); - field.at('foo').setValue(111); + field.at('aaa').observe(element); + field.at('aaa').setValue(222); - expect(element.value).toBe('111'); + expect(element.value).toBe('222'); }); test('sets the initial value to the element', () => { - const field = createField({ foo: 111 }, uncontrolledPlugin()); + const field = createField({ aaa: 111 }, uncontrolledPlugin()); element.type = 'number'; - field.at('foo').ref(element); + field.at('aaa').observe(element); expect(element.value).toBe('111'); }); @@ -53,14 +53,14 @@ describe('uncontrolledPlugin', () => { field.ref = refMock; }); - const field = createField({ foo: 111 }, composePlugins(pluginMock, uncontrolledPlugin())); + const field = createField({ aaa: 111 }, composePlugins(pluginMock, uncontrolledPlugin())); expect(pluginMock).toHaveBeenCalledTimes(1); - expect(pluginMock).toHaveBeenNthCalledWith(1, field, naturalAccessor, expect.any(Function)); + expect(pluginMock).toHaveBeenNthCalledWith(1, field); expect(refMock).not.toHaveBeenCalled(); - field.at('foo').ref(element); + field.at('aaa').observe(element); expect(refMock).toHaveBeenCalledTimes(1); expect(refMock).toHaveBeenNthCalledWith(1, element); @@ -75,10 +75,10 @@ describe('uncontrolledPlugin', () => { const element1 = document.body.appendChild(document.createElement('input')); const element2 = document.body.appendChild(document.createElement('input')); - const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); + const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').ref(element1); - field.at('foo').ref(element2); + field.at('aaa').observe(element1); + field.at('aaa').observe(element2); expect(refMock).toHaveBeenCalledTimes(1); expect(refMock).toHaveBeenNthCalledWith(1, element1); @@ -93,10 +93,10 @@ describe('uncontrolledPlugin', () => { const element1 = document.body.appendChild(document.createElement('input')); const element2 = document.body.appendChild(document.createElement('textarea')); - const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); + const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').ref(element1); - field.at('foo').ref(element2); + field.at('aaa').observe(element1); + field.at('aaa').observe(element2); expect(refMock).toHaveBeenCalledTimes(1); expect(refMock).toHaveBeenNthCalledWith(1, element1); @@ -116,9 +116,9 @@ describe('uncontrolledPlugin', () => { field.ref = refMock; }; - const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); + const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').ref(element); + field.at('aaa').observe(element); element.remove(); @@ -136,9 +136,9 @@ describe('uncontrolledPlugin', () => { field.ref = refMock; }; - const field = createField({ foo: 111 }, composePlugins(plugin, uncontrolledPlugin())); + const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - field.at('foo').ref(null); + field.at('aaa').observe(null); expect(refMock).not.toHaveBeenCalled(); }); @@ -153,7 +153,7 @@ describe('uncontrolledPlugin', () => { const setValueMock = (field.setValue = jest.fn(field.setValue)); - field.ref(element); + field.observe(element); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); @@ -176,17 +176,17 @@ describe('uncontrolledPlugin', () => { set: jest.fn(), }; - const field = createField({ foo: 'aaa' }, uncontrolledPlugin(accessorMock)); + const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('foo').ref(element); + field.at('aaa').observe(element); expect(accessorMock.set).toHaveBeenCalledTimes(1); - expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); + expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 111); - field.at('foo').setValue('bbb'); + field.at('aaa').setValue(222); expect(accessorMock.set).toHaveBeenCalledTimes(2); - expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element], 'bbb'); + expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element], 222); }); test('does not call set accessor if there are no referenced elements', () => { @@ -195,9 +195,9 @@ describe('uncontrolledPlugin', () => { set: jest.fn(), }; - const field = createField({ foo: 'aaa' }, uncontrolledPlugin(accessorMock)); + const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('foo').setValue('bbb'); + field.at('aaa').setValue(222); expect(accessorMock.set).not.toHaveBeenCalled(); }); @@ -211,14 +211,14 @@ describe('uncontrolledPlugin', () => { const element1 = document.body.appendChild(document.createElement('input')); const element2 = document.body.appendChild(document.createElement('textarea')); - const field = createField({ foo: 111 }, uncontrolledPlugin(accessorMock)); + const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('foo').ref(element1); + field.at('aaa').observe(element1); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element1], 111); - field.at('foo').ref(element2); + field.at('aaa').observe(element2); expect(accessorMock.set).toHaveBeenCalledTimes(2); expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element1, element2], 111); @@ -232,9 +232,9 @@ describe('uncontrolledPlugin', () => { const element = document.createElement('input'); - const field = createField({ foo: 111 }, uncontrolledPlugin(accessorMock)); + const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('foo').ref(element); + field.at('aaa').observe(element); expect(accessorMock.set).toHaveBeenCalledTimes(0); }); @@ -242,9 +242,9 @@ describe('uncontrolledPlugin', () => { test('mutation observer disconnects after last element is removed', done => { const disconnectMock = jest.spyOn(MutationObserver.prototype, 'disconnect'); - const field = createField({ foo: 111 }, uncontrolledPlugin()); + const field = createField({ aaa: 111 }, uncontrolledPlugin()); - field.at('foo').ref(element); + field.at('aaa').observe(element); element.remove(); diff --git a/packages/zod-plugin/README.md b/packages/zod-plugin/README.md index 98f4262a..b318fd1b 100644 --- a/packages/zod-plugin/README.md +++ b/packages/zod-plugin/README.md @@ -27,13 +27,12 @@ export const App = () => { event.preventDefault(); if (planetField.validate()) { - // Errors are associated with fields automatically - return; + // If your shapes have transformations or refinements, you can safely parse + // the field value after it was successfully validated. + const value = planetSchema.parse(planetField.value); + } else { + // Errors are associated with fields automatically. } - - // If your shapes have transformations or refinements, you can safely parse - // the field value after it was successfully validated - const value = planetSchema.parse(planetField.value); }; return ( diff --git a/packages/zod-plugin/package.json b/packages/zod-plugin/package.json index eb1e90ed..4cc9aa7f 100644 --- a/packages/zod-plugin/package.json +++ b/packages/zod-plugin/package.json @@ -20,7 +20,8 @@ "scripts": { "build": "rollup --config ../../rollup.config.js", "clean": "rimraf lib", - "test": "jest --config ../../jest.config.js" + "test": "jest --config ../../jest.config.js", + "test:definitions": "tsd --files './src/test/**/*.test-d.ts'" }, "repository": { "type": "git", diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index 71a2c7db..1ff30b5f 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -66,7 +66,7 @@ function getValue(field: Field): unknown { while (field.parent !== null) { transient ||= field.isTransient; - value = transient ? field.accessor.set(field.parent.value, field.key, value) : field.parent.value; + value = transient ? field.valueAccessor.set(field.parent.value, field.key, value) : field.parent.value; field = field.parent; } return value; diff --git a/packages/zod-plugin/src/test/zodPlugin.test-d.ts b/packages/zod-plugin/src/test/zodPlugin.test-d.ts index 4ab0429e..3c7a2e1f 100644 --- a/packages/zod-plugin/src/test/zodPlugin.test-d.ts +++ b/packages/zod-plugin/src/test/zodPlugin.test-d.ts @@ -5,6 +5,4 @@ import { ZodPlugin, zodPlugin } from '@roqueform/zod-plugin'; const shape = z.object({ foo: z.object({ bar: z.string() }) }); -expectType & ZodPlugin>( - createField({ foo: { bar: 'aaa' } }, zodPlugin(shape)) -); +expectType>(createField({ foo: { bar: 'aaa' } }, zodPlugin(shape))); diff --git a/packages/zod-plugin/src/test/zodPlugin.test.tsx b/packages/zod-plugin/src/test/zodPlugin.test.tsx index 771d0a37..8cca31ec 100644 --- a/packages/zod-plugin/src/test/zodPlugin.test.tsx +++ b/packages/zod-plugin/src/test/zodPlugin.test.tsx @@ -3,204 +3,181 @@ import { zodPlugin } from '../main'; import { createField } from 'roqueform'; describe('zodPlugin', () => { - const fooType = z.object({ - foo: z.number().gte(3), + const aaaType = z.object({ + aaa: z.number().gte(3), }); - const fooBarType = z.object({ - foo: z.number().min(3), - bar: z.string().max(2), + const aaaBbbType = z.object({ + aaa: z.number().min(3), + bbb: z.string().max(2), }); test('enhances the field', () => { - const field = createField({ foo: 0 }, zodPlugin(fooType)); + const field = createField({ aaa: 111 }, zodPlugin(aaaType)); expect(field.isInvalid).toBe(false); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toBe(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toBeNull(); }); test('converts string errors to issue messages', () => { - const field = createField({ foo: 0 }, zodPlugin(fooType)); + const field = createField({ aaa: 111 }, zodPlugin(aaaType)); - field.setError('aaa'); + field.setError('xxx'); - expect(field.error).toEqual({ code: ZodIssueCode.custom, message: 'aaa', path: [] }); + expect(field.error).toEqual({ code: ZodIssueCode.custom, message: 'xxx', path: [] }); }); test('sets issue as an error', () => { - const field = createField({ foo: 0 }, zodPlugin(fooType)); + const field = createField({ aaa: 111 }, zodPlugin(aaaType)); const issue: ZodIssue = { code: ZodIssueCode.custom, path: ['bbb'], message: 'aaa' }; - field.at('foo').setError(issue); + field.at('aaa').setError(issue); - expect(field.at('foo').error).toBe(issue); - expect(field.at('foo').error).toEqual({ code: ZodIssueCode.custom, message: 'aaa', path: ['bbb'] }); + expect(field.at('aaa').error).toBe(issue); + expect(field.at('aaa').error).toEqual({ code: ZodIssueCode.custom, message: 'aaa', path: ['bbb'] }); }); test('validates the root field', () => { - const field = createField({ foo: 0 }, zodPlugin(fooType)); + const field = createField({ aaa: 0 }, zodPlugin(aaaType)); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'too_small', exact: false, inclusive: true, message: 'Number must be greater than or equal to 3', minimum: 3, - path: ['foo'], + path: ['aaa'], type: 'number', }); }); test('validates the child field', () => { - const field = createField({ foo: 0 }, zodPlugin(fooType)); + const field = createField({ aaa: 0 }, zodPlugin(aaaType)); - field.at('foo').validate(); + field.at('aaa').validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'too_small', exact: false, inclusive: true, message: 'Number must be greater than or equal to 3', minimum: 3, - path: ['foo'], + path: ['aaa'], type: 'number', }); }); test('validates multiple fields', () => { - const field = createField({ foo: 0, bar: 'qux' }, zodPlugin(fooBarType)); + const field = createField({ aaa: 0, bbb: 'ccc' }, zodPlugin(aaaBbbType)); field.validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'too_small', exact: false, inclusive: true, message: 'Number must be greater than or equal to 3', minimum: 3, - path: ['foo'], + path: ['aaa'], type: 'number', }); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toEqual({ + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toEqual({ code: 'too_big', exact: false, inclusive: true, maximum: 2, message: 'String must contain at most 2 character(s)', - path: ['bar'], + path: ['bbb'], type: 'string', }); }); test('does not validate sibling fields', () => { - const field = createField({ foo: 0, bar: 'qux' }, zodPlugin(fooBarType)); + const field = createField({ aaa: 111, bbb: 'ccc' }, zodPlugin(aaaBbbType)); - field.at('bar').validate(); + field.at('bbb').validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toEqual(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toEqual(null); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toEqual({ + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toEqual({ code: 'too_big', exact: false, inclusive: true, maximum: 2, message: 'String must contain at most 2 character(s)', - path: ['bar'], + path: ['bbb'], type: 'string', }); }); test('validates a transient value', () => { - const field = createField({ foo: 0, bar: '' }, zodPlugin(fooBarType)); + const field = createField({ aaa: 111, bbb: '' }, zodPlugin(aaaBbbType)); - field.at('bar').setTransientValue('qux'); - field.at('bar').validate(); + field.at('bbb').setTransientValue('ccc'); + field.at('bbb').validate(); expect(field.isInvalid).toBe(true); - expect(field.error).toBe(null); + expect(field.error).toBeNull(); - expect(field.at('foo').isInvalid).toBe(false); - expect(field.at('foo').error).toEqual(null); + expect(field.at('aaa').isInvalid).toBe(false); + expect(field.at('aaa').error).toEqual(null); - expect(field.at('bar').isInvalid).toBe(true); - expect(field.at('bar').error).toEqual({ + expect(field.at('bbb').isInvalid).toBe(true); + expect(field.at('bbb').error).toEqual({ code: 'too_big', exact: false, inclusive: true, maximum: 2, message: 'String must contain at most 2 character(s)', - path: ['bar'], + path: ['bbb'], type: 'string', }); }); - test('uses errorMap passed to createField', () => { - const errorMapMock: ZodErrorMap = jest.fn(() => { - return { message: 'aaa' }; - }); - - const field = createField({ foo: 0, bar: '' }, zodPlugin(fooBarType, errorMapMock)); - - field.validate(); - - expect(errorMapMock).toHaveBeenCalledTimes(1); - - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ - code: 'too_small', - exact: false, - inclusive: true, - message: 'aaa', - minimum: 3, - path: ['foo'], - type: 'number', - }); - }); - test('uses errorMap passed to validate method', () => { const errorMapMock: ZodErrorMap = jest.fn(() => { return { message: 'aaa' }; }); - const field = createField({ foo: 0, bar: '' }, zodPlugin(fooBarType)); + const field = createField({ aaa: 0, bbb: '' }, zodPlugin(aaaBbbType)); field.validate({ errorMap: errorMapMock }); expect(errorMapMock).toHaveBeenCalledTimes(1); - expect(field.at('foo').isInvalid).toBe(true); - expect(field.at('foo').error).toEqual({ + expect(field.at('aaa').isInvalid).toBe(true); + expect(field.at('aaa').error).toEqual({ code: 'too_small', exact: false, inclusive: true, message: 'aaa', minimum: 3, - path: ['foo'], + path: ['aaa'], type: 'number', }); }); diff --git a/typedoc.json b/typedoc.json index 2a9557b8..2fc8f2c6 100644 --- a/typedoc.json +++ b/typedoc.json @@ -34,6 +34,7 @@ "Promise": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" }, "global": { + "MutationObserver": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/MutationObserver", "Date": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date", "File": "https://developer.mozilla.org/en-US/docs/Web/API/File" } From d0c2df5720fcbe0d008f7bbc26f4ea4e0947c8ab Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Fri, 10 Nov 2023 17:55:15 +0300 Subject: [PATCH 12/33] Removed AnyField --- packages/doubter-plugin/src/main/doubterPlugin.ts | 12 ++++++++++-- packages/react/src/main/FieldRenderer.ts | 8 +++++--- packages/roqueform/src/main/typings.ts | 11 ++--------- packages/zod-plugin/src/main/zodPlugin.ts | 12 ++++++++++-- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index 9b4416fc..246bf418 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -1,5 +1,13 @@ import { Err, Issue, Ok, ParseOptions, Shape } from 'doubter'; -import { AnyField, Field, PluginInjector, Validation, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; +import { + Field, + FieldController, + PluginInjector, + Validation, + ValidationPlugin, + validationPlugin, + Validator, +} from 'roqueform'; /** * The plugin added to fields by the {@link doubterPlugin}. @@ -64,7 +72,7 @@ const doubterValidator: Validator = { }, }; -function prependPath(field: AnyField, issue: Issue): Issue { +function prependPath(field: FieldController, issue: Issue): Issue { while (field.parent !== null) { (issue.path ||= []).unshift(field.key); field = field.parent; diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index 356aa54f..b4028ef8 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -1,12 +1,12 @@ import { createElement, Fragment, ReactElement, ReactNode, useEffect, useReducer, useRef } from 'react'; -import { AnyField, callOrGet, ValueOf } from 'roqueform'; +import { callOrGet, FieldController, ValueOf } from 'roqueform'; /** * Properties of the {@link FieldRenderer} component. * * @template RenderedField The rendered field. */ -export interface FieldRendererProps { +export interface FieldRendererProps> { /** * The field that triggers re-renders. */ @@ -39,7 +39,9 @@ export interface FieldRendererProps { * * @template RenderedField The rendered field. */ -export function FieldRenderer(props: FieldRendererProps): ReactElement { +export function FieldRenderer>( + props: FieldRendererProps +): ReactElement { const { field, eagerlyUpdated } = props; const [, rerender] = useReducer(reduceCount, 0); diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 04c36b15..7777f0ef 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -1,10 +1,3 @@ -/** - * The field that doesn't constrain its children and ancestors. Use this in plugins to streamline typing. - * - * @template Plugin The plugin injected into the field. - */ -export type AnyField = FieldController & Plugin; - /** * The field that manages a value and related data. Fields can be {@link PluginInjector enhanced by plugins} that * provide integration with rendering frameworks, validation libraries, and other tools. @@ -20,7 +13,7 @@ export type Field = FieldController { +export interface Event, Data = any> { /** * The type of the event. */ @@ -50,7 +43,7 @@ export interface Event { * @template Target The field where the event is dispatched. * @template Data The additional data related to the event. */ -export type Subscriber = (event: Event) => void; +export type Subscriber, Data = any> = (event: Event) => void; /** * Unsubscribes the subscriber. No-op if subscriber was already unsubscribed. diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index 1ff30b5f..8ec68e8e 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -1,5 +1,13 @@ import { ParseParams, SafeParseReturnType, ZodIssue, ZodIssueCode, ZodSchema, ZodTypeAny } from 'zod'; -import { AnyField, Field, PluginInjector, Validation, ValidationPlugin, validationPlugin, Validator } from 'roqueform'; +import { + Field, + FieldController, + PluginInjector, + Validation, + ValidationPlugin, + validationPlugin, + Validator, +} from 'roqueform'; /** * The plugin added to fields by the {@link zodPlugin}. @@ -72,7 +80,7 @@ function getValue(field: Field): unknown { return value; } -function getPath(field: AnyField): any[] { +function getPath(field: FieldController): any[] { const path = []; while (field.parent !== null) { From cf44c10139e01de1052303e8591fdb2770183da1 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Fri, 10 Nov 2023 18:21:37 +0300 Subject: [PATCH 13/33] Fixed setError --- .../src/main/constraintValidationPlugin.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 0c886de9..56981569 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -207,7 +207,8 @@ function setError( return events; } - if (isValidatable(field.element)) { + if (errorOrigin === 2 && isValidatable(field.element)) { + // Custom validation error field.element.setCustomValidity(error); } @@ -232,19 +233,19 @@ function setError( } function deleteError(field: Field, errorOrigin: 1 | 2, events: Event[]): Event[] { - const originalError = field.error; + const { error: originalError, element } = field; if (field.errorOrigin > errorOrigin || originalError === null) { return events; } - if (isValidatable(field.element)) { - field.element.setCustomValidity(''); + if (isValidatable(element)) { + element.setCustomValidity(''); - if (!field.element.validity.valid) { + if (!element.validity.valid) { field.errorOrigin = 1; - if (originalError !== (field.error = field.element.validationMessage)) { + if (originalError !== (field.error = element.validationMessage)) { events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); } return events; From 57241881b3f59f164e6b86267ca37a3bb68d3bda Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Fri, 10 Nov 2023 18:58:47 +0300 Subject: [PATCH 14/33] Fixed FieldRenderer rendering --- packages/react/src/main/FieldRenderer.ts | 32 +++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index b4028ef8..b65d61c3 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -1,4 +1,4 @@ -import { createElement, Fragment, ReactElement, ReactNode, useEffect, useReducer, useRef } from 'react'; +import { createElement, Fragment, ReactElement, ReactNode, useLayoutEffect, useReducer, useRef } from 'react'; import { callOrGet, FieldController, ValueOf } from 'roqueform'; /** @@ -49,21 +49,23 @@ export function FieldRenderer>( handleChangeRef.current = props.onChange; - useEffect(() => { - return field.on('*', event => { - if (eagerlyUpdated || event.origin === field) { - rerender(); - } - if (field.isTransient || event.type !== 'change:value') { - return; - } + useLayoutEffect( + () => + field.on('*', event => { + if (eagerlyUpdated || event.origin === field) { + rerender(); + } + if (field.isTransient || event.type !== 'change:value') { + return; + } - const handleChange = handleChangeRef.current; - if (typeof handleChange === 'function') { - handleChange(field.value); - } - }); - }, [field, eagerlyUpdated]); + const handleChange = handleChangeRef.current; + if (typeof handleChange === 'function') { + handleChange(field.value); + } + }), + [field, eagerlyUpdated] + ); return createElement(Fragment, null, callOrGet(props.children, field)); } From 84e1b6e85f61dd71e309e3b6df1ff09e22cab6b8 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Fri, 10 Nov 2023 19:16:52 +0300 Subject: [PATCH 15/33] Added isFocused --- packages/ref-plugin/src/main/refPlugin.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index 6d5dae3a..087b8161 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -9,6 +9,11 @@ export interface RefPlugin { */ element: Element | null; + /** + * `true` if the {@link element DOM element} is focused, `false` otherwise. + */ + readonly isFocused: boolean; + /** * Associates the field with the {@link element DOM element}. */ @@ -50,6 +55,11 @@ export function refPlugin(): PluginInjector { return field => { field.element = null; + Object.defineProperty(field, 'isFocused', { + configurable: true, + get: () => field.element !== null && field.element.ownerDocument.activeElement === field.element, + }); + const { ref } = field; field.ref = element => { From 7e224a88e030d7f8b47486faf514259628646061 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Fri, 10 Nov 2023 21:07:20 +0300 Subject: [PATCH 16/33] Check element is connected --- .../src/main/scrollToErrorPlugin.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 222c8bda..76aed6b5 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -97,7 +97,7 @@ function getTargetFields( field: Field, batch: Field[] ): Field[] { - if (field.error !== null && field.element !== null) { + if (field.error !== null && field.element !== null && field.element.isConnected) { const rect = field.element.getBoundingClientRect(); if (rect.top !== 0 || rect.left !== 0 || rect.width !== 0 || rect.height !== 0) { @@ -113,7 +113,11 @@ function getTargetFields( } function sortByBoundingRect(fields: Field[], rtl: boolean): Field[] { - const { body, documentElement } = document; + if (fields.length === 0) { + return fields; + } + + const { body, documentElement } = fields[0].element!.ownerDocument; const scrollY = window.pageYOffset || documentElement.scrollTop || body.scrollTop; const clientY = documentElement.clientTop || body.clientTop || 0; From 868a3ab6e8ba00d3162dca51bdcc1391315ea614 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Fri, 10 Nov 2023 22:41:52 +0300 Subject: [PATCH 17/33] Event bubbling --- .../src/main/constraintValidationPlugin.ts | 29 +++++++----- packages/react/src/main/FieldRenderer.ts | 5 +- packages/reset-plugin/src/main/resetPlugin.ts | 14 +++++- packages/roqueform/src/main/createField.ts | 6 +-- packages/roqueform/src/main/typings.ts | 25 +++++----- packages/roqueform/src/main/utils.ts | 47 +++++++++++++------ .../roqueform/src/main/validationPlugin.ts | 35 +++++--------- .../roqueform/src/test/createField.test.ts | 2 +- .../src/main/uncontrolledPlugin.ts | 8 ++-- 9 files changed, 97 insertions(+), 74 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 56981569..98721748 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -1,4 +1,13 @@ -import { dispatchEvents, Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform'; +import { + createEvent, + dispatchEvents, + Event, + Field, + PluginInjector, + PluginOf, + Subscriber, + Unsubscribe, +} from 'roqueform'; const EVENT_CHANGE_ERROR = 'change:error'; @@ -115,7 +124,7 @@ export function constraintValidationPlugin(): PluginInjector { - if (field.element === event.target && isValidatable(field.element)) { + if (field.element === event.currentTarget && isValidatable(field.element)) { dispatchEvents(setError(field, field.element.validationMessage, 1, [])); } }; @@ -215,7 +224,7 @@ function setError( field.error = error; field.errorOrigin = errorOrigin; - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); if (originalError !== null) { return events; @@ -224,11 +233,8 @@ function setError( field.errorCount++; for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { - if (ancestor.errorCount++ === 0) { - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); - } + ancestor.errorCount++; } - return events; } @@ -246,7 +252,7 @@ function deleteError(field: Field, errorOrigin: 1 | field.errorOrigin = 1; if (originalError !== (field.error = element.validationMessage)) { - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); } return events; } @@ -256,14 +262,11 @@ function deleteError(field: Field, errorOrigin: 1 | field.errorOrigin = 0; field.errorCount--; - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { - if (--ancestor.errorCount === 0) { - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); - } + ancestor.errorCount--; } - return events; } diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index b65d61c3..b1a8de9c 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -52,10 +52,11 @@ export function FieldRenderer>( useLayoutEffect( () => field.on('*', event => { - if (eagerlyUpdated || event.origin === field) { + if (eagerlyUpdated || event.target === field) { rerender(); } - if (field.isTransient || event.type !== 'change:value') { + if (field.isTransient || event.type !== 'change:value' || event.target !== field) { + // The non-transient value of this field didn't change return; } diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 658497ed..8de9891e 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,4 +1,14 @@ -import { dispatchEvents, Event, Field, isEqual, PluginInjector, Subscriber, Unsubscribe, ValueOf } from 'roqueform'; +import { + createEvent, + dispatchEvents, + Event, + Field, + isEqual, + PluginInjector, + Subscriber, + Unsubscribe, + ValueOf, +} from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; /** @@ -89,7 +99,7 @@ function propagateInitialValue( initialValue: unknown, events: Event[] ): Event[] { - events.push({ type: 'change:initialValue', origin: target, target: field, data: field.initialValue }); + events.unshift(createEvent('change:initialValue', field, initialValue)); field.initialValue = initialValue; diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index bd1fd3df..0c4dd0eb 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,5 +1,5 @@ -import { ValueAccessor, Event, Field, PluginInjector, Subscriber } from './typings'; -import { callOrGet, dispatchEvents, isEqual } from './utils'; +import { Event, Field, PluginInjector, Subscriber, ValueAccessor } from './typings'; +import { callOrGet, createEvent, dispatchEvents, isEqual } from './utils'; import { naturalAccessor } from './naturalAccessor'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types @@ -132,7 +132,7 @@ function setValue(field: Field, value: unknown, transient: boolean): void { } function propagateValue(origin: Field, field: Field, value: unknown, events: Event[]): Event[] { - events.push({ type: 'change:value', origin, target: field, data: field.value }); + events.unshift(createEvent('change:value', field, value)); field.value = value; diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 7777f0ef..60e4bbd1 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -10,28 +10,27 @@ export type Field = FieldController, Data = any> { +export interface Event { /** * The type of the event. */ type: string; /** - * The field that has changed. + * The field to which the event subscriber has been added. */ - target: Target; + currentTarget: CurrentTarget; /** - * The original field that caused the event to be dispatched. This can be ancestor, descendant, or the{@link target} - * itself. + * The field onto which the event was dispatched. */ - origin: Field>; + target: Field>; /** - * The additional data related to the event, depends on the {@link type event type}. + * The {@link type type-specific} data related to the {@link target target field}. */ data: Data; } @@ -40,10 +39,10 @@ export interface Event, Data = any> { * The callback that receives events dispatched by {@link Field a field}. * * @param event The dispatched event. - * @template Target The field where the event is dispatched. + * @template CurrentTarget The field where the event is dispatched. * @template Data The additional data related to the event. */ -export type Subscriber, Data = any> = (event: Event) => void; +export type Subscriber = (event: Event) => void; /** * Unsubscribes the subscriber. No-op if subscriber was already unsubscribed. @@ -58,7 +57,7 @@ export type Unsubscribe = () => void; * * @template T The field to infer plugin of. */ -export type PluginOf = T['__plugin__' & keyof T]; +export type PluginOf = '__plugin__' extends keyof T ? T['__plugin__'] : unknown; /** * Infers the value of the field. @@ -67,7 +66,7 @@ export type PluginOf = T['__plugin__' & keyof T]; * * @template T The field to infer value of. */ -export type ValueOf = T['value' & keyof T]; +export type ValueOf = 'value' extends keyof T ? T['value'] : unknown; /** * The field controller provides the core field functionality. @@ -211,7 +210,7 @@ export interface FieldController { on(eventType: '*', subscriber: Subscriber): Unsubscribe; /** - * Subscribes to {@link value the field value} changes. + * Subscribes to {@link value the field value} changes. {@link Event.data} contains the previous field value. * * @param eventType The type of the event. * @param subscriber The subscriber that would be triggered. diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 0cb52e57..d4ecb431 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,4 +1,4 @@ -import { Event } from './typings'; +import { Event, FieldController } from './typings'; /** * [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison operation. @@ -24,6 +24,21 @@ export function callOrGet(value: T | ((arg: A) => T), arg: A): T { return typeof value === 'function' ? (value as Function)(arg) : value; } +/** + * Creates the new event that would be dispatched from target field. + * + * @param type The type of the event. + * @param target The target field from which the event is dispatched. + * @param data The data carried by the event. + */ +export function createEvent, Data>( + type: string, + target: Target, + data: Data +): Event { + return { type, currentTarget: target, target, data }; +} + /** * Calls field subscribers that can handle given events. * @@ -31,23 +46,27 @@ export function callOrGet(value: T | ((arg: A) => T), arg: A): T { */ export function dispatchEvents(events: readonly Event[]): void { for (const event of events) { - const { subscribers } = event.target; + for (let field = event.currentTarget; field !== null; field = field.parent) { + const { subscribers } = field; - if (subscribers === null) { - continue; - } + if (subscribers === null) { + continue; + } - const typeSubscribers = subscribers[event.type]; - const globSubscribers = subscribers['*']; + event.currentTarget = field; - if (typeSubscribers !== undefined) { - for (const subscriber of typeSubscribers) { - subscriber(event); + const typeSubscribers = subscribers[event.type]; + const globSubscribers = subscribers['*']; + + if (typeSubscribers !== undefined) { + for (const subscriber of typeSubscribers) { + subscriber(event); + } } - } - if (globSubscribers !== undefined) { - for (const subscriber of globSubscribers) { - subscriber(event); + if (globSubscribers !== undefined) { + for (const subscriber of globSubscribers) { + subscriber(event); + } } } } diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 55e9708a..48b144ff 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,5 +1,5 @@ import { Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from './typings'; -import { dispatchEvents, isEqual } from './utils'; +import { createEvent, dispatchEvents, isEqual } from './utils'; const EVENT_CHANGE_ERROR = 'change:error'; const ERROR_ABORT = 'Validation aborted'; @@ -129,7 +129,7 @@ export interface ValidationPlugin { abortValidation(): void; /** - * Subscribes to {@link error an associated error} changes of this field or any of its descendants. + * Subscribes to {@link error an associated error} changes. {@link Event.data} contains the previous error. * * @param eventType The type of the event. * @param subscriber The subscriber that would be triggered. @@ -140,9 +140,7 @@ export interface ValidationPlugin { on(eventType: 'change:error', subscriber: Subscriber): Unsubscribe; /** - * Subscribes to the start of the validation. The event is triggered for all fields that are going to be validated. - * The {@link FieldController.value current value} of the field is the one that is being validated. - * {@link Event.origin} points to the field where validation was triggered. + * Subscribes to the start of the validation. {@link Event.data} carries the validation that is going to start. * * @param eventType The type of the event. * @param subscriber The subscriber that would be triggered. @@ -150,12 +148,11 @@ export interface ValidationPlugin { * @see {@link validation} * @see {@link isValidating} */ - on(eventType: 'validation:start', subscriber: Subscriber): Unsubscribe; + on(eventType: 'validation:start', subscriber: Subscriber>>): Unsubscribe; /** - * Subscribes to the end of the validation. The event is triggered for all fields that were validated when validation. - * {@link Event.origin} points to the field where validation was triggered. Check {@link isInvalid} to detect the - * actual validity status. + * Subscribes to the end of the validation. Check {@link isInvalid} to detect the actual validity status. + * {@link Event.data} carries the validation that has ended. * * @param eventType The type of the event. * @param subscriber The subscriber that would be triggered. @@ -163,7 +160,7 @@ export interface ValidationPlugin { * @see {@link validation} * @see {@link isValidating} */ - on(eventType: 'validation:end', subscriber: Subscriber): Unsubscribe; + on(eventType: 'validation:end', subscriber: Subscriber>>): Unsubscribe; /** * Associates a validation error with the field and notifies the subscribers. Use this method in @@ -297,7 +294,7 @@ function setError(field: Field, error: unknown, errorOrigin: 1 field.error = error; field.errorOrigin = errorOrigin; - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); if (originalError !== null) { return events; @@ -306,11 +303,8 @@ function setError(field: Field, error: unknown, errorOrigin: 1 field.errorCount++; for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { - if (ancestor.errorCount++ === 0) { - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); - } + ancestor.errorCount++; } - return events; } @@ -325,14 +319,11 @@ function deleteError(field: Field, errorOrigin: 1 | 2, events: field.errorOrigin = 0; field.errorCount--; - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: field, data: originalError }); + events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { - if (--ancestor.errorCount === 0) { - events.push({ type: EVENT_CHANGE_ERROR, origin: field, target: ancestor, data: originalError }); - } + ancestor.errorCount--; } - return events; } @@ -353,7 +344,7 @@ function clearErrors(field: Field, errorOrigin: 1 | 2, events: function startValidation(field: Field, validation: Validation, events: Event[]): Event[] { field.validation = validation; - events.push({ type: 'validation:start', origin: validation.root, target: field, data: undefined }); + events.push(createEvent('validation:start', field, validation)); if (field.children !== null) { for (const child of field.children) { @@ -372,7 +363,7 @@ function endValidation(field: Field, validation: Validation, e field.validation = null; - events.push({ type: 'validation:end', origin: validation.root, target: field, data: undefined }); + events.push(createEvent('validation:end', field, validation)); if (field.children !== null) { for (const child of field.children) { diff --git a/packages/roqueform/src/test/createField.test.ts b/packages/roqueform/src/test/createField.test.ts index ef0ead25..f1df5cd6 100644 --- a/packages/roqueform/src/test/createField.test.ts +++ b/packages/roqueform/src/test/createField.test.ts @@ -350,7 +350,7 @@ describe('createField', () => { const newValue = { aaa: 222 }; field.at('aaa').on('*', event => { - expect(event.origin.value).toBe(newValue); + expect(event.target.value).toBe(newValue); done(); }); diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index 347e7131..a4f6b7ad 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -1,4 +1,4 @@ -import { dispatchEvents, Event, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; +import { createEvent, dispatchEvents, Event, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; @@ -91,7 +91,7 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec element.removeEventListener('change', changeListener); observedElements.splice(elementIndex, 1); - events.push({ type: EVENT_CHANGE_OBSERVED_ELEMENTS, origin: field, target: field, data: element }); + events.push(createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)); } } @@ -108,7 +108,7 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec const changeListener: EventListener = event => { let value; if ( - field.observedElements.indexOf(event.target as Element) !== -1 && + field.observedElements.indexOf(event.currentTarget as Element) !== -1 && !isDeepEqual((value = field.elementValueAccessor.get(field.observedElements)), field.value) ) { field.setValue(value); @@ -146,7 +146,7 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec field.ref?.(observedElements[0]); } - dispatchEvents([{ type: EVENT_CHANGE_OBSERVED_ELEMENTS, origin: field, target: field, data: element }]); + dispatchEvents([createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)]); }; field.setElementValueAccessor = accessor => { From eedb7019d602d1143ae03f5c3004f6d32024f82c Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 00:25:49 +0300 Subject: [PATCH 18/33] scrollToError returns field --- .../src/main/constraintValidationPlugin.ts | 9 +++------ packages/ref-plugin/src/main/refPlugin.ts | 2 +- .../src/main/scrollToErrorPlugin.ts | 15 ++++++++------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 98721748..59a108e5 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -133,27 +133,23 @@ export function constraintValidationPlugin(): PluginInjector { if (field.element === element) { - // Same element ref?.(element); return; } if (field.element !== null) { - // Disconnect current element + // Disconnect from the current element field.element.removeEventListener('input', changeListener); field.element.removeEventListener('change', changeListener); field.element.removeEventListener('invalid', changeListener); - - field.element = null; } field.element = element; - field.validity = null; const events: Event[] = []; if (isValidatable(element)) { - // Connect new element + // Connect to the new element element.addEventListener('input', changeListener); element.addEventListener('change', changeListener); element.addEventListener('invalid', changeListener); @@ -162,6 +158,7 @@ export function constraintValidationPlugin(): PluginInjector { const { ref } = field; field.ref = element => { - field.element = element instanceof Element ? element : null; + field.element = element; ref?.(element); }; diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 76aed6b5..f62cbbf4 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -1,4 +1,4 @@ -import { Field, PluginInjector } from 'roqueform'; +import { Field, PluginInjector, PluginOf } from 'roqueform'; export interface ScrollToErrorOptions extends ScrollIntoViewOptions { /** @@ -38,9 +38,9 @@ export interface ScrollToErrorPlugin { * @param alignToTop If `true`, the top of the element will be aligned to the top of the visible area of the * scrollable ancestor, otherwise element will be aligned to the bottom of the visible area of the scrollable * ancestor. - * @returns `true` if there's an error to scroll to, or `false` otherwise. + * @returns The field which is scrolled to, or `null` if there's no scroll happening. */ - scrollToError(index?: number, alignToTop?: boolean): boolean; + scrollToError(index?: number, alignToTop?: boolean): Field> | null; /** * Scroll to the element that is referenced by a field that has an associated error. Scrolls the field element's @@ -52,9 +52,9 @@ export interface ScrollToErrorPlugin { * the end of the sequence. `scrollToError(-1)` scroll to the last error. The order of errors is the same as the * visual order of fields left-to-right and top-to-bottom. * @param options [The scroll options.](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#sect1) - * @returns `true` if there's an error to scroll to, or `false` otherwise. + * @returns The field which is scrolled to, or `null` if there's no scroll happening. */ - scrollToError(index?: number, options?: ScrollToErrorOptions): boolean; + scrollToError(index?: number, options?: ScrollToErrorOptions): Field> | null; } /** @@ -80,15 +80,16 @@ export function scrollToErrorPlugin(): PluginInjector { const targets = getTargetFields(field, []); if (targets.length === 0) { - return false; + return null; } const target = sortByBoundingRect(targets, rtl)[index < 0 ? targets.length + index : index]; if (target !== undefined) { target.element!.scrollIntoView(options); + return target; } - return true; + return null; }; }; } From 09a8b946301ca2a35b4fc0cf5f8f3d2a64d7b0ad Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 01:04:29 +0300 Subject: [PATCH 19/33] Fixed end validation if start has thrown --- .../roqueform/src/main/validationPlugin.ts | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 48b144ff..338ace7f 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -412,7 +412,12 @@ function validate(field: Field, options: unknown): boolean { const validation: Validation = { root: field, abortController: null }; - dispatchEvents(startValidation(field, validation, [])); + try { + dispatchEvents(startValidation(field, validation, [])); + } catch (error) { + dispatchEvents(endValidation(field, validation, [])); + throw error; + } if (field.validation !== validation) { throw new Error(ERROR_ABORT); @@ -433,42 +438,49 @@ function validate(field: Field, options: unknown): boolean { } function validateAsync(field: Field, options: unknown): Promise { - dispatchEvents(clearErrors(field, 1, abortValidation(field, []))); - - if (field.validation !== null) { - return Promise.reject(new Error(ERROR_ABORT)); - } - - const validation: Validation = { root: field, abortController: new AbortController() }; + return new Promise((resolve, reject) => { + dispatchEvents(clearErrors(field, 1, abortValidation(field, []))); - dispatchEvents(startValidation(field, validation, [])); + if (field.validation !== null) { + reject(new Error(ERROR_ABORT)); + return; + } - if ((field.validation as Validation | null) !== validation) { - return Promise.reject(new Error(ERROR_ABORT)); - } + const validation: Validation = { root: field, abortController: new AbortController() }; - const { validate, validateAsync = validate } = field.validator; - - return Promise.race([ - new Promise(resolve => { - resolve(validateAsync(field, options)); - }), - new Promise((_resolve, reject) => { - validation.abortController!.signal.addEventListener('abort', () => { - reject(new Error(ERROR_ABORT)); - }); - }), - ]).then( - () => { - if (field.validation !== validation) { - throw new Error(ERROR_ABORT); - } + try { + dispatchEvents(startValidation(field, validation, [])); + } catch (error) { dispatchEvents(endValidation(field, validation, [])); - return field.errorCount === 0; - }, - error => { - dispatchEvents(endValidation(field, validation, [])); - throw error; + reject(error); + return; + } + + if ((field.validation as Validation | null) !== validation || validation.abortController === null) { + reject(new Error(ERROR_ABORT)); + return; } - ); + + const { validate, validateAsync = validate } = field.validator; + + validation.abortController.signal.addEventListener('abort', () => { + reject(new Error(ERROR_ABORT)); + }); + + resolve( + Promise.resolve(validateAsync(field, options)).then( + () => { + if (field.validation !== validation) { + throw new Error(ERROR_ABORT); + } + dispatchEvents(endValidation(field, validation, [])); + return field.errorCount === 0; + }, + error => { + dispatchEvents(endValidation(field, validation, [])); + throw error; + } + ) + ); + }); } From 0db3369c92909c8d147fa7a32abae09bbaa2f9b1 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 02:36:22 +0300 Subject: [PATCH 20/33] Fixed dispatch --- .../src/main/constraintValidationPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 59a108e5..c91c3e2a 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -162,9 +162,9 @@ export function constraintValidationPlugin(): PluginInjector { From 0b4d9d5af7ba631b4c8160cc1abdaa67cbae76b8 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 16:38:02 +0300 Subject: [PATCH 21/33] Added getDirtyFields --- packages/ref-plugin/src/main/refPlugin.ts | 2 +- packages/reset-plugin/src/main/resetPlugin.ts | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index 277a0622..3c8d00f4 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -10,7 +10,7 @@ export interface RefPlugin { element: Element | null; /** - * `true` if the {@link element DOM element} is focused, `false` otherwise. + * `true` if the {@link element DOM element} is currently focused, `false` otherwise. */ readonly isFocused: boolean; diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 8de9891e..68b36fcf 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -5,6 +5,7 @@ import { Field, isEqual, PluginInjector, + PluginOf, Subscriber, Unsubscribe, ValueOf, @@ -16,9 +17,10 @@ import isDeepEqual from 'fast-deep-equal'; */ export interface ResetPlugin { /** - * `true` if the field value is different from its initial value, or `false` otherwise. + * `true` if the field value is different from its initial value basing on {@link equalityChecker equality checker}, + * or `false` otherwise. */ - readonly isDirty: boolean; + isDirty: boolean; /** * The callback that compares initial value and the current value of the field. @@ -41,6 +43,14 @@ export interface ResetPlugin { */ reset(): void; + /** + * Returns all fields that have {@link FieldController.value a value} that is different from + * {@link FieldController.initialValue an initial value} basing on {@link equalityChecker equality checker}. + * + * @see {@link isDirty} + */ + getDirtyFields(): Field>[]; + /** * Subscribes to changes of {@link FieldController.initialValue the initial value}. * @@ -75,6 +85,8 @@ export function resetPlugin( field.reset = () => { field.setValue(field.initialValue); }; + + field.getDirtyFields = () => getDirtyFields(field, []); }; } @@ -114,3 +126,15 @@ function propagateInitialValue( } return events; } + +function getDirtyFields(field: Field, batch: Field[]): Field[] { + if (field.isDirty) { + batch.push(field); + } + if (field.children !== null) { + for (const child of field.children) { + getDirtyFields(child, batch); + } + } + return batch; +} From bc71871e31d75b7e73e798d816092475c7a05bf6 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 18:17:23 +0300 Subject: [PATCH 22/33] Added hasFocus --- packages/ref-plugin/src/main/refPlugin.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index 3c8d00f4..fc791cbd 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -10,9 +10,9 @@ export interface RefPlugin { element: Element | null; /** - * `true` if the {@link element DOM element} is currently focused, `false` otherwise. + * `true` if {@link element the DOM element} or any of its descendants have focus, `false` otherwise. */ - readonly isFocused: boolean; + readonly hasFocus: boolean; /** * Associates the field with the {@link element DOM element}. @@ -55,15 +55,15 @@ export function refPlugin(): PluginInjector { return field => { field.element = null; - Object.defineProperty(field, 'isFocused', { + Object.defineProperty(field, 'hasFocus', { configurable: true, - get: () => field.element !== null && field.element.ownerDocument.activeElement === field.element, + get: () => field.element !== null && field.element.contains(field.element.ownerDocument.activeElement), }); const { ref } = field; field.ref = element => { - field.element = element; + field.element = element instanceof Element ? element : null; ref?.(element); }; From ef8a1319dc61be8b983674314818b2f1ab337207 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 18:17:36 +0300 Subject: [PATCH 23/33] More docs --- packages/reset-plugin/src/main/resetPlugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 68b36fcf..a86f02c7 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -27,6 +27,7 @@ export interface ResetPlugin { * * @param initialValue The initial value. * @param value The current value. + * @returns `true` if initial value is equal to value, or `false` otherwise. * @protected */ ['equalityChecker']: (initialValue: any, value: any) => boolean; From dba01ffd3118afa5d09ce754aa701577fc2e7fa1 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 18:18:24 +0300 Subject: [PATCH 24/33] More defensive refs --- .../src/main/constraintValidationPlugin.ts | 7 ++++--- .../scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index c91c3e2a..760b6d09 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -144,8 +144,6 @@ export function constraintValidationPlugin(): PluginInjector { const { ref } = field; field.ref = element => { - field.element = element; + field.element = element instanceof Element ? element : null; ref?.(element); }; From 263b785a7cfca4a871057dacfada747980a29ee8 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 19:22:15 +0300 Subject: [PATCH 25/33] Removed setElementValueAccessor; Fixed check constraints on value update --- .../src/main/constraintValidationPlugin.ts | 9 +++--- .../src/main/createElementValueAccessor.ts | 4 +-- .../src/main/uncontrolledPlugin.ts | 29 +++++-------------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 760b6d09..2e159d29 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -123,12 +123,14 @@ export function constraintValidationPlugin(): PluginInjector field.errorCount !== 0 }, }); - const changeListener: EventListener = event => { - if (field.element === event.currentTarget && isValidatable(field.element)) { + const changeListener = () => { + if (isValidatable(field.element)) { dispatchEvents(setError(field, field.element.validationMessage, 1, [])); } }; + field.on('change:value', changeListener); + const { ref } = field; field.ref = element => { @@ -138,7 +140,6 @@ export function constraintValidationPlugin(): PluginInjector { const events: Event[] = []; const { observedElements } = field; - const [element] = observedElements; for (const mutation of mutations) { for (let i = 0; i < mutation.removedNodes.length; ++i) { @@ -98,7 +90,7 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec if (observedElements.length === 0) { mutationObserver.disconnect(); field.ref?.(null); - } else if (element !== observedElements[0]) { + } else { field.ref?.(observedElements[0]); } @@ -125,33 +117,28 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec const { observedElements } = field; if ( - element === null || !(element instanceof Element) || !element.isConnected || - observedElements.indexOf(element) !== -1 + element.parentNode === null || + observedElements.includes(element) ) { return; } - mutationObserver.observe(element.parentNode!, { childList: true }); + mutationObserver.observe(element.parentNode, { childList: true }); element.addEventListener('input', changeListener); element.addEventListener('change', changeListener); - observedElements.push(element); + const elementCount = observedElements.push(element); field.elementValueAccessor.set(observedElements, field.value); - if (observedElements.length === 1) { - field.ref?.(observedElements[0]); + if (elementCount === 1) { + field.ref?.(element); } dispatchEvents([createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)]); }; - - field.setElementValueAccessor = accessor => { - field.elementValueAccessor = accessor; - return field; - }; }; } From ad804d97e121823e4f061c2f10fa5cdeca065f21 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sat, 11 Nov 2023 23:16:01 +0300 Subject: [PATCH 26/33] Removed MutationObserver; Added refFor --- .../src/main/constraintValidationPlugin.ts | 38 ++-- packages/ref-plugin/src/main/refPlugin.ts | 2 +- .../src/main/scrollToErrorPlugin.ts | 2 +- .../src/main/uncontrolledPlugin.ts | 168 ++++++++---------- 4 files changed, 100 insertions(+), 110 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 2e159d29..ed5c5241 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -133,37 +133,37 @@ export function constraintValidationPlugin(): PluginInjector { - if (field.element === element) { - ref?.(element); + field.ref = nextElement => { + const prevElement = field.element; + + ref?.(nextElement); + + if (prevElement === nextElement) { return; } - if (field.element !== null) { - field.element.removeEventListener('input', changeListener); - field.element.removeEventListener('change', changeListener); - field.element.removeEventListener('invalid', changeListener); + field.element = nextElement = nextElement instanceof Element ? nextElement : null; + + if (prevElement !== null) { + prevElement.removeEventListener('input', changeListener); + prevElement.removeEventListener('change', changeListener); + prevElement.removeEventListener('invalid', changeListener); } const events: Event[] = []; - if (isValidatable(element)) { - element.addEventListener('input', changeListener); - element.addEventListener('change', changeListener); - element.addEventListener('invalid', changeListener); + if (isValidatable(nextElement)) { + nextElement.addEventListener('input', changeListener); + nextElement.addEventListener('change', changeListener); + nextElement.addEventListener('invalid', changeListener); - field.element = element; - field.validity = element.validity; - - setError(field, element.validationMessage, 1, events); + field.validity = nextElement.validity; + setError(field, nextElement.validationMessage, 1, events); } else { - field.element = field.validity = null; - + field.validity = null; deleteError(field, 1, events); } - ref?.(element); - dispatchEvents(events); }; diff --git a/packages/ref-plugin/src/main/refPlugin.ts b/packages/ref-plugin/src/main/refPlugin.ts index fc791cbd..894433eb 100644 --- a/packages/ref-plugin/src/main/refPlugin.ts +++ b/packages/ref-plugin/src/main/refPlugin.ts @@ -63,8 +63,8 @@ export function refPlugin(): PluginInjector { const { ref } = field; field.ref = element => { - field.element = element instanceof Element ? element : null; ref?.(element); + field.element = element instanceof Element ? element : null; }; field.scrollIntoView = options => { diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index a8adf91e..5d79959f 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -71,8 +71,8 @@ export function scrollToErrorPlugin(): PluginInjector { const { ref } = field; field.ref = element => { - field.element = element instanceof Element ? element : null; ref?.(element); + field.element = element instanceof Element ? element : null; }; field.scrollToError = (index = 0, options) => { diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index 045d6419..4e3e0a22 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -1,9 +1,7 @@ -import { createEvent, dispatchEvents, Event, PluginInjector, Subscriber, Unsubscribe } from 'roqueform'; +import { Field, PluginInjector } from 'roqueform'; import isDeepEqual from 'fast-deep-equal'; import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; -const EVENT_CHANGE_OBSERVED_ELEMENTS = 'change:observedElements'; - /** * The default value accessor. */ @@ -13,132 +11,124 @@ const elementValueAccessor = createElementValueAccessor(); * The plugin added to fields by the {@link uncontrolledPlugin}. */ export interface UncontrolledPlugin { - /** - * The array of elements that are used to derive the field value. Update this array by calling {@link observe} method. - * Elements are observed by the {@link !MutationObserver MutationObserver} and deleted from this array when they are - * removed from DOM. - * - * @protected - */ - ['observedElements']: Element[]; + element: Element | null; /** - * The accessor that reads and writes field value from and to {@link observedElements observed elements}. + * The accessor that reads and writes the field value from and to {@link elements}. * * @protected */ ['elementValueAccessor']: ElementValueAccessor; /** - * Adds the DOM element to {@link observedElements observed elements}. - * - * @param element The element to observe. No-op if the element is `null` or not connected to the DOM. - */ - observe(element: Element | null): void; - - /** - * Subscribes to updates of {@link observedElements observed elements}. - * - * @param eventType The type of the event. - * @param subscriber The subscriber that would be triggered. - * @returns The callback to unsubscribe the subscriber. + * Associates the field with the DOM element. */ - on(eventType: 'change:observedElements', subscriber: Subscriber): Unsubscribe; + ref(element: Element | null): void; /** - * Associates the field with {@link element the DOM element}. This method is usually exposed by plugins that use DOM - * element references. This method is invoked when {@link observedElements the first observed element} is changed. - * - * @protected + * Returns a callback that associates the field with the DOM element under the given key. */ - ['ref']?(element: Element | null): void; + refFor(key: unknown): (element: Element | null) => void; } /** * Updates field value when the DOM element value is changed and vice versa. - * - * @param accessor The accessor that reads and writes values to and from the DOM elements that - * {@link UncontrolledPlugin.observedElements are observed by the filed}. */ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjector { return field => { - field.observedElements = []; field.elementValueAccessor = accessor; - const mutationObserver = new MutationObserver(mutations => { - const events: Event[] = []; - const { observedElements } = field; + const refs = new Map void>(); + const elements: Element[] = []; + const elementMap = new Map(); - for (const mutation of mutations) { - for (let i = 0; i < mutation.removedNodes.length; ++i) { - const elementIndex = observedElements.indexOf(mutation.removedNodes.item(i) as Element); - - if (elementIndex === -1) { - continue; - } - - const element = observedElements[elementIndex]; - - element.removeEventListener('input', changeListener); - element.removeEventListener('change', changeListener); - - observedElements.splice(elementIndex, 1); - events.push(createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)); - } - } - - if (observedElements.length === 0) { - mutationObserver.disconnect(); - field.ref?.(null); - } else { - field.ref?.(observedElements[0]); - } - - dispatchEvents(events); - }); + let prevValue = field.value; const changeListener: EventListener = event => { let value; if ( - field.observedElements.indexOf(event.currentTarget as Element) !== -1 && - !isDeepEqual((value = field.elementValueAccessor.get(field.observedElements)), field.value) + elements.includes(event.target as Element) && + !isDeepEqual((value = field.elementValueAccessor.get(elements)), field.value) ) { - field.setValue(value); + field.setValue((prevValue = value)); } }; - field.on('change:value', () => { - if (field.observedElements.length !== 0) { - field.elementValueAccessor.set(field.observedElements, field.value); + field.on('change:value', event => { + if (field.value !== prevValue && event.target === field && elements.length !== 0) { + field.elementValueAccessor.set(elements, field.value); } }); - field.observe = element => { - const { observedElements } = field; + const { ref } = field; - if ( - !(element instanceof Element) || - !element.isConnected || - element.parentNode === null || - observedElements.includes(element) - ) { - return; - } + field.ref = nextElement => { + const prevElement = field.element; - mutationObserver.observe(element.parentNode, { childList: true }); + ref?.(nextElement); - element.addEventListener('input', changeListener); - element.addEventListener('change', changeListener); + field.element = swapElements(field, changeListener, elements, prevElement, nextElement); + }; - const elementCount = observedElements.push(element); + field.refFor = key => { + let ref = refs.get(key); + if (ref !== undefined) { + return ref; + } - field.elementValueAccessor.set(observedElements, field.value); + ref = nextElement => { + const prevElement = elementMap.get(key) || null; - if (elementCount === 1) { - field.ref?.(element); - } + nextElement = swapElements(field, changeListener, elements, prevElement, nextElement); - dispatchEvents([createEvent(EVENT_CHANGE_OBSERVED_ELEMENTS, field, element)]); + if (prevElement !== nextElement) { + elementMap.set(key, nextElement); + } + }; + refs.set(key, ref); + return ref; }; }; } + +function swapElements( + field: Field, + changeListener: EventListener, + elements: Element[], + prevElement: Element | null, + nextElement: Element | null +): Element | null { + nextElement = nextElement instanceof Element ? nextElement : null; + + if (prevElement === nextElement) { + return nextElement; + } + + let prevIndex = -1; + + if (prevElement !== null) { + prevElement.removeEventListener('input', changeListener); + prevElement.removeEventListener('change', changeListener); + prevIndex = elements.indexOf(prevElement); + } + + if (nextElement !== null) { + nextElement.addEventListener('input', changeListener); + nextElement.addEventListener('change', changeListener); + + if (prevIndex === -1) { + elements.push(nextElement); + } else { + elements[prevIndex] = nextElement; + prevIndex = -1; + } + } + + if (prevIndex !== -1) { + elements.splice(prevIndex, 1); + } + + field.elementValueAccessor.set(elements, field.value); + + return nextElement; +} From 85ea024f4e2effaa63fc8bcebdffad2e95a6411d Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sun, 12 Nov 2023 18:26:44 +0300 Subject: [PATCH 27/33] Added readonly --- packages/reset-plugin/src/main/resetPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index a86f02c7..ebfb76c1 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -20,7 +20,7 @@ export interface ResetPlugin { * `true` if the field value is different from its initial value basing on {@link equalityChecker equality checker}, * or `false` otherwise. */ - isDirty: boolean; + readonly isDirty: boolean; /** * The callback that compares initial value and the current value of the field. From 1415672b74a045fd811d965702475ef189d46ca0 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sun, 12 Nov 2023 18:41:45 +0300 Subject: [PATCH 28/33] Fixed same element set to different refs --- .../uncontrolled-plugin/src/main/uncontrolledPlugin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index 4e3e0a22..5c976fa1 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -112,7 +112,7 @@ function swapElements( prevIndex = elements.indexOf(prevElement); } - if (nextElement !== null) { + if (nextElement !== null && elements.indexOf(nextElement) === -1) { nextElement.addEventListener('input', changeListener); nextElement.addEventListener('change', changeListener); @@ -127,8 +127,8 @@ function swapElements( if (prevIndex !== -1) { elements.splice(prevIndex, 1); } - - field.elementValueAccessor.set(elements, field.value); - + if (elements.length !== 0) { + field.elementValueAccessor.set(elements, field.value); + } return nextElement; } From e37af89bde2378eecb7cc47f20b6d7481beda8f6 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sun, 12 Nov 2023 21:19:13 +0300 Subject: [PATCH 29/33] Readonly arrays and maps --- packages/roqueform/src/main/createField.ts | 4 +- packages/roqueform/src/main/typings.ts | 12 +-- packages/roqueform/src/main/utils.ts | 4 +- packages/uncontrolled-plugin/README.md | 8 +- packages/uncontrolled-plugin/package.json | 3 - ...ssor.ts => createElementsValueAccessor.ts} | 22 ++--- .../uncontrolled-plugin/src/main/index.ts | 2 +- .../src/main/uncontrolledPlugin.ts | 94 ++++++++++++------- .../test/createElementValueAccessor.test.ts | 42 ++++----- .../src/test/uncontrolledPlugin.test.ts | 12 +-- 10 files changed, 108 insertions(+), 95 deletions(-) rename packages/uncontrolled-plugin/src/main/{createElementValueAccessor.ts => createElementsValueAccessor.ts} (91%) diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 0c4dd0eb..43b4e4b4 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -107,8 +107,8 @@ function getOrCreateField( plugin?.(child); if (parent !== null) { - (parent.children ||= []).push(child); - (parent.childrenMap ||= new Map()).set(child.key, child); + ((parent.children ||= []) as Field[]).push(child); + ((parent.childrenMap ||= new Map()) as Map).set(child.key, child); } return child; diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 60e4bbd1..3a677277 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -88,7 +88,7 @@ export interface FieldController { * The key in the {@link parent parent value} that corresponds to the value of this field, or `null` if there's no * parent. */ - key: any; + readonly key: any; /** * The current value of the field. @@ -124,22 +124,16 @@ export interface FieldController { /** * The array of immediate child fields that were {@link at previously accessed}, or `null` if there are no children. * - * This array is populated during {@link FieldController.at} call. - * - * @see {@link childrenMap} * @protected */ - ['children']: Field[] | null; + ['children']: readonly Field[] | null; /** * Mapping from a key to a corresponding child field, or `null` if there are no children. * - * This map is populated during {@link FieldController.at} call. - * - * @see {@link children} * @protected */ - ['childrenMap']: Map> | null; + ['childrenMap']: ReadonlyMap> | null; /** * The map from an event type to an array of associated subscribers, or `null` if no subscribers were added. diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index d4ecb431..ee717234 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -28,7 +28,7 @@ export function callOrGet(value: T | ((arg: A) => T), arg: A): T { * Creates the new event that would be dispatched from target field. * * @param type The type of the event. - * @param target The target field from which the event is dispatched. + * @param target The target field where the event is dispatched. * @param data The data carried by the event. */ export function createEvent, Data>( @@ -40,7 +40,7 @@ export function createEvent, Data>( } /** - * Calls field subscribers that can handle given events. + * Dispatches multiple events to field subscribers. * * @param events The array of events to dispatch. */ diff --git a/packages/uncontrolled-plugin/README.md b/packages/uncontrolled-plugin/README.md index 152519e6..1211f839 100644 --- a/packages/uncontrolled-plugin/README.md +++ b/packages/uncontrolled-plugin/README.md @@ -122,9 +122,9 @@ values of form elements: - `null`, `undefined`, `NaN` and non-finite numbers are coerced to an empty string and written to `value` attribute. This behaviour can be changed by passing a custom -[`ElementValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/uncontrolled_plugin.ElementValueAccessor.html) +[`ElementsValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/uncontrolled_plugin.ElementsValueAccessor.html) implementation to a plugin. Or you can use a -[`createElementValueAccessor`](https://smikhalevski.github.io/roqueform/functions/uncontrolled_plugin.createElementValueAccessor.html) +[`createElementsValueAccessor`](https://smikhalevski.github.io/roqueform/functions/uncontrolled_plugin.createElementsValueAccessor.html) factory to customise the default behaviour: ```ts @@ -134,10 +134,10 @@ import { uncontrolledPlugin } from '@roqueform/uncontrolled-plugin'; const personField = useField( { dateOfBirth: 316310400000 }, uncontrolledPlugin( - createElementValueAccessor({ dateFormat: 'timestamp' }) + createElementsValueAccessor({ dateFormat: 'timestamp' }) ) ); ``` Read more about available options in -[`ElementValueAccessorOptions`](https://smikhalevski.github.io/roqueform/interfaces/uncontrolled_plugin.ElementValueAccessorOptions.html). +[`ElementsValueAccessorOptions`](https://smikhalevski.github.io/roqueform/interfaces/uncontrolled_plugin.ElementsValueAccessorOptions.html). diff --git a/packages/uncontrolled-plugin/package.json b/packages/uncontrolled-plugin/package.json index 9b9e5244..508daba9 100644 --- a/packages/uncontrolled-plugin/package.json +++ b/packages/uncontrolled-plugin/package.json @@ -42,8 +42,5 @@ "homepage": "https://github.com/smikhalevski/roqueform/tree/master/packages/uncontrolled-plugin#readme", "peerDependencies": { "roqueform": "^4.0.0" - }, - "dependencies": { - "fast-deep-equal": "^3.1.3" } } diff --git a/packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts b/packages/uncontrolled-plugin/src/main/createElementsValueAccessor.ts similarity index 91% rename from packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts rename to packages/uncontrolled-plugin/src/main/createElementsValueAccessor.ts index f1d3b322..bd777c70 100644 --- a/packages/uncontrolled-plugin/src/main/createElementValueAccessor.ts +++ b/packages/uncontrolled-plugin/src/main/createElementsValueAccessor.ts @@ -1,7 +1,7 @@ /** - * Abstraction over DOM element value getter and setter. + * Abstraction over value getter and setter for a group of DOM elements. */ -export interface ElementValueAccessor { +export interface ElementsValueAccessor { /** * Retrieves value from elements that produce value for the field. * @@ -20,9 +20,9 @@ export interface ElementValueAccessor { } /** - * Options applied to {@link createElementValueAccessor}. + * Options applied to {@link createElementsValueAccessor}. */ -export interface ElementValueAccessorOptions { +export interface ElementsValueAccessorOptions { /** * The format of checkbox values. * @@ -104,10 +104,10 @@ export interface ElementValueAccessorOptions { * * By default: * - * - Single checkbox → boolean, see {@link ElementValueAccessorOptions.checkboxFormat}; + * - Single checkbox → boolean, see {@link ElementsValueAccessorOptions.checkboxFormat}; * - Multiple checkboxes → an array of *
value - * attributes of checked checkboxes, see {@link ElementValueAccessorOptions.checkboxFormat}; + * attributes of checked checkboxes, see {@link ElementsValueAccessorOptions.checkboxFormat}; * - Radio buttons → the * value * attribute of a radio button that is checked or `null` if no radio buttons are checked; @@ -115,8 +115,8 @@ export interface ElementValueAccessorOptions { * - Range input → number; * - Date input → the * value - * attribute, or `null` if empty, see {@link ElementValueAccessorOptions.dateFormat}; - * - Time input → a time string, or `null` if empty, see {@link ElementValueAccessorOptions.timeFormat}; + * attribute, or `null` if empty, see {@link ElementsValueAccessorOptions.dateFormat}; + * - Time input → a time string, or `null` if empty, see {@link ElementsValueAccessorOptions.timeFormat}; * - Image input → string value of the * value * attribute; @@ -125,10 +125,10 @@ export interface ElementValueAccessorOptions { * - Others → The _value_ attribute, or `null` if element doesn't support it; * - `null`, `undefined`, `NaN` and non-finite numbers are coerced to an empty string and written to _value_ attribute. */ -export function createElementValueAccessor(options: ElementValueAccessorOptions = {}): ElementValueAccessor { +export function createElementsValueAccessor(options: ElementsValueAccessorOptions = {}): ElementsValueAccessor { const { checkboxFormat, dateFormat, timeFormat } = options; - const get: ElementValueAccessor['get'] = elements => { + const get: ElementsValueAccessor['get'] = elements => { const element = elements[0]; const { type, valueAsNumber } = element; @@ -202,7 +202,7 @@ export function createElementValueAccessor(options: ElementValueAccessorOptions return element.value; }; - const set: ElementValueAccessor['set'] = (elements, value) => { + const set: ElementsValueAccessor['set'] = (elements, value) => { const element = elements[0]; const { type } = element; diff --git a/packages/uncontrolled-plugin/src/main/index.ts b/packages/uncontrolled-plugin/src/main/index.ts index 081639ed..3102257f 100644 --- a/packages/uncontrolled-plugin/src/main/index.ts +++ b/packages/uncontrolled-plugin/src/main/index.ts @@ -2,5 +2,5 @@ * @module uncontrolled-plugin */ -export * from './createElementValueAccessor'; +export * from './createElementsValueAccessor'; export * from './uncontrolledPlugin'; diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index 5c976fa1..106b0251 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -1,24 +1,41 @@ import { Field, PluginInjector } from 'roqueform'; -import isDeepEqual from 'fast-deep-equal'; -import { createElementValueAccessor, ElementValueAccessor } from './createElementValueAccessor'; +import { createElementsValueAccessor, ElementsValueAccessor } from './createElementsValueAccessor'; /** * The default value accessor. */ -const elementValueAccessor = createElementValueAccessor(); +const elementsValueAccessor = createElementsValueAccessor(); /** * The plugin added to fields by the {@link uncontrolledPlugin}. */ export interface UncontrolledPlugin { + /** + * The DOM element associated with the field, or `null` if there's no associated element. + */ element: Element | null; + /** + * The array of elements controlled by this field, includes {@link element}. + * + * @protected + */ + ['elements']: readonly Element[]; + + /** + * The map from a key passed to {@link refFor} to a corresponding element. If {@link refFor} was called with `null` + * then a key is deleted from this map. + * + * @protected + */ + ['elementsMap']: ReadonlyMap; + /** * The accessor that reads and writes the field value from and to {@link elements}. * * @protected */ - ['elementValueAccessor']: ElementValueAccessor; + ['elementsValueAccessor']: ElementsValueAccessor; /** * Associates the field with the DOM element. @@ -26,7 +43,7 @@ export interface UncontrolledPlugin { ref(element: Element | null): void; /** - * Returns a callback that associates the field with the DOM element under the given key. + * Returns a callback that associates the field with the DOM element under the given key in {@link elementsMap}. */ refFor(key: unknown): (element: Element | null) => void; } @@ -34,29 +51,25 @@ export interface UncontrolledPlugin { /** * Updates field value when the DOM element value is changed and vice versa. */ -export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjector { +export function uncontrolledPlugin(accessor = elementsValueAccessor): PluginInjector { return field => { - field.elementValueAccessor = accessor; + field.elements = []; + field.elementsMap = new Map(); + field.elementsValueAccessor = accessor; - const refs = new Map void>(); - const elements: Element[] = []; - const elementMap = new Map(); + const refsMap = new Map void>(); let prevValue = field.value; const changeListener: EventListener = event => { - let value; - if ( - elements.includes(event.target as Element) && - !isDeepEqual((value = field.elementValueAccessor.get(elements)), field.value) - ) { - field.setValue((prevValue = value)); + if (field.elements.includes(event.target as Element)) { + field.setValue((prevValue = field.elementsValueAccessor.get(field.elements))); } }; field.on('change:value', event => { - if (field.value !== prevValue && event.target === field && elements.length !== 0) { - field.elementValueAccessor.set(elements, field.value); + if (field.value !== prevValue && event.target === field && field.elements.length !== 0) { + field.elementsValueAccessor.set(field.elements, field.value); } }); @@ -67,25 +80,31 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec ref?.(nextElement); - field.element = swapElements(field, changeListener, elements, prevElement, nextElement); + field.element = swapElements(field, changeListener, prevElement, nextElement); }; field.refFor = key => { - let ref = refs.get(key); - if (ref !== undefined) { - return ref; + let ref = refsMap.get(key); + + if (ref === undefined) { + ref = nextElement => { + const elementsMap = field.elementsMap as Map; + const prevElement = elementsMap.get(key) || null; + + nextElement = swapElements(field, changeListener, prevElement, nextElement); + + if (prevElement === nextElement) { + return; + } + if (nextElement === null) { + elementsMap.delete(key); + } else { + elementsMap.set(key, nextElement); + } + }; + refsMap.set(key, ref); } - ref = nextElement => { - const prevElement = elementMap.get(key) || null; - - nextElement = swapElements(field, changeListener, elements, prevElement, nextElement); - - if (prevElement !== nextElement) { - elementMap.set(key, nextElement); - } - }; - refs.set(key, ref); return ref; }; }; @@ -94,10 +113,11 @@ export function uncontrolledPlugin(accessor = elementValueAccessor): PluginInjec function swapElements( field: Field, changeListener: EventListener, - elements: Element[], prevElement: Element | null, nextElement: Element | null ): Element | null { + const elements = field.elements as Element[]; + nextElement = nextElement instanceof Element ? nextElement : null; if (prevElement === nextElement) { @@ -113,8 +133,10 @@ function swapElements( } if (nextElement !== null && elements.indexOf(nextElement) === -1) { - nextElement.addEventListener('input', changeListener); - nextElement.addEventListener('change', changeListener); + nextElement.addEventListener( + nextElement.tagName === 'INPUT' || nextElement.tagName === 'TEXTAREA' ? 'input' : 'change', + changeListener + ); if (prevIndex === -1) { elements.push(nextElement); @@ -128,7 +150,7 @@ function swapElements( elements.splice(prevIndex, 1); } if (elements.length !== 0) { - field.elementValueAccessor.set(elements, field.value); + field.elementsValueAccessor.set(elements, field.value); } return nextElement; } diff --git a/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts b/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts index fee6be37..2652aad8 100644 --- a/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts +++ b/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts @@ -1,7 +1,7 @@ -import { createElementValueAccessor } from '../main'; +import { createElementsValueAccessor } from '../main'; -describe('createElementValueAccessor', () => { - const accessor = createElementValueAccessor(); +describe('createElementsValueAccessor', () => { + const accessor = createElementsValueAccessor(); function createElement(tagName: string, attributes?: object): any { return Object.assign(document.createElement(tagName), attributes); @@ -35,14 +35,14 @@ describe('createElementValueAccessor', () => { }); test('returns boolean for a single checkbox for "auto" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'auto' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'auto' }); expect(accessor.get([createElement('input', { type: 'checkbox', checked: true, value: 'aaa' })])).toBe(true); expect(accessor.get([createElement('input', { type: 'checkbox', checked: false, value: 'aaa' })])).toBe(false); }); test('returns an array of checked values for multiple checkboxes for "auto" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'auto' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'auto' }); expect( accessor.get([ @@ -54,14 +54,14 @@ describe('createElementValueAccessor', () => { }); test('returns boolean for a single checkbox for "boolean" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'boolean' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'boolean' }); expect(accessor.get([createElement('input', { type: 'checkbox', checked: true, value: 'aaa' })])).toBe(true); expect(accessor.get([createElement('input', { type: 'checkbox', checked: false, value: 'aaa' })])).toBe(false); }); test('returns an array of booleans for multiple checkboxes for "boolean" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'boolean' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'boolean' }); expect( accessor.get([ @@ -73,7 +73,7 @@ describe('createElementValueAccessor', () => { }); test('returns an array of booleans for a single checkbox for "booleanArray" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'booleanArray' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'booleanArray' }); expect(accessor.get([createElement('input', { type: 'checkbox', checked: true, value: 'aaa' })])).toEqual([true]); expect(accessor.get([createElement('input', { type: 'checkbox', checked: false, value: 'aaa' })])).toEqual([ @@ -82,7 +82,7 @@ describe('createElementValueAccessor', () => { }); test('returns an array of booleans for multiple checkboxes for "booleanArray" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'booleanArray' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'booleanArray' }); expect( accessor.get([ @@ -94,14 +94,14 @@ describe('createElementValueAccessor', () => { }); test('returns value or null for a single checkbox for "value" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'value' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'value' }); expect(accessor.get([createElement('input', { type: 'checkbox', checked: true, value: 'aaa' })])).toBe('aaa'); expect(accessor.get([createElement('input', { type: 'checkbox', checked: false, value: 'aaa' })])).toBeNull(); }); test('returns an array of values for multiple checkboxes for "value" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'value' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'value' }); expect( accessor.get([ @@ -113,7 +113,7 @@ describe('createElementValueAccessor', () => { }); test('returns an array of values for a single checkbox for "valueArray" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'valueArray' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'valueArray' }); expect(accessor.get([createElement('input', { type: 'checkbox', checked: true, value: 'aaa' })])).toEqual([ 'aaa', @@ -122,7 +122,7 @@ describe('createElementValueAccessor', () => { }); test('returns an array of booleans for multiple checkboxes for "valueArray" format', () => { - const accessor = createElementValueAccessor({ checkboxFormat: 'valueArray' }); + const accessor = createElementsValueAccessor({ checkboxFormat: 'valueArray' }); expect( accessor.get([ @@ -175,7 +175,7 @@ describe('createElementValueAccessor', () => { }); test('returns input value for date inputs for "value" format', () => { - const accessor = createElementValueAccessor({ dateFormat: 'value' }); + const accessor = createElementsValueAccessor({ dateFormat: 'value' }); const value1 = accessor.get([createElement('input', { type: 'date', valueAsNumber: 1676475065312 })]); const value2 = accessor.get([createElement('input', { type: 'datetime-local', valueAsNumber: 1676475065312 })]); @@ -185,7 +185,7 @@ describe('createElementValueAccessor', () => { }); test('returns ISO date for date inputs for "iso" format', () => { - const accessor = createElementValueAccessor({ dateFormat: 'iso' }); + const accessor = createElementsValueAccessor({ dateFormat: 'iso' }); const value1 = accessor.get([createElement('input', { type: 'date', valueAsNumber: 1676475065312 })]); const value2 = accessor.get([createElement('input', { type: 'datetime-local', valueAsNumber: 1676475065312 })]); @@ -195,7 +195,7 @@ describe('createElementValueAccessor', () => { }); test('returns UTC date for date inputs for "utc" format', () => { - const accessor = createElementValueAccessor({ dateFormat: 'utc' }); + const accessor = createElementsValueAccessor({ dateFormat: 'utc' }); const value1 = accessor.get([createElement('input', { type: 'date', valueAsNumber: 1676475065312 })]); const value2 = accessor.get([createElement('input', { type: 'datetime-local', valueAsNumber: 1676475065312 })]); @@ -205,7 +205,7 @@ describe('createElementValueAccessor', () => { }); test('returns GMT date for date inputs for "gmt" format', () => { - const accessor = createElementValueAccessor({ dateFormat: 'gmt' }); + const accessor = createElementsValueAccessor({ dateFormat: 'gmt' }); const value1 = accessor.get([createElement('input', { type: 'date', valueAsNumber: 1676475065312 })]); const value2 = accessor.get([createElement('input', { type: 'datetime-local', valueAsNumber: 1676475065312 })]); @@ -215,7 +215,7 @@ describe('createElementValueAccessor', () => { }); test('returns Date object for date inputs for "object" format', () => { - const accessor = createElementValueAccessor({ dateFormat: 'object' }); + const accessor = createElementsValueAccessor({ dateFormat: 'object' }); const value1 = accessor.get([createElement('input', { type: 'date', valueAsNumber: 1676475065312 })]); const value2 = accessor.get([createElement('input', { type: 'datetime-local', valueAsNumber: 1676475065312 })]); @@ -225,7 +225,7 @@ describe('createElementValueAccessor', () => { }); test('returns timestamp for date inputs for "timestamp" format', () => { - const accessor = createElementValueAccessor({ dateFormat: 'timestamp' }); + const accessor = createElementsValueAccessor({ dateFormat: 'timestamp' }); const value1 = accessor.get([createElement('input', { type: 'date', valueAsNumber: 1676475065312 })]); const value2 = accessor.get([createElement('input', { type: 'datetime-local', valueAsNumber: 1676475065312 })]); @@ -243,13 +243,13 @@ describe('createElementValueAccessor', () => { }); test('returns value for time inputs for "value" format', () => { - const accessor = createElementValueAccessor({ timeFormat: 'value' }); + const accessor = createElementsValueAccessor({ timeFormat: 'value' }); expect(accessor.get([createElement('input', { type: 'time', valueAsNumber: 12300000 })])).toBe('03:25'); }); test('returns number for time inputs for "number" format', () => { - const accessor = createElementValueAccessor({ timeFormat: 'number' }); + const accessor = createElementsValueAccessor({ timeFormat: 'number' }); expect(accessor.get([createElement('input', { type: 'time', valueAsNumber: 12300000 })])).toBe(12300000); }); diff --git a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts index a5341703..be4124af 100644 --- a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts +++ b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts @@ -1,4 +1,4 @@ -import { ElementValueAccessor, uncontrolledPlugin } from '../main'; +import { ElementsValueAccessor, uncontrolledPlugin } from '../main'; import { composePlugins, createField } from 'roqueform'; import { fireEvent } from '@testing-library/dom'; @@ -144,7 +144,7 @@ describe('uncontrolledPlugin', () => { }); test('does not call setValue if the same value multiple times', () => { - const accessorMock: ElementValueAccessor = { + const accessorMock: ElementsValueAccessor = { get: jest.fn(() => 'xxx'), set: jest.fn(), }; @@ -171,7 +171,7 @@ describe('uncontrolledPlugin', () => { }); test('uses accessor to set values to the element', () => { - const accessorMock: ElementValueAccessor = { + const accessorMock: ElementsValueAccessor = { get: () => undefined, set: jest.fn(), }; @@ -190,7 +190,7 @@ describe('uncontrolledPlugin', () => { }); test('does not call set accessor if there are no referenced elements', () => { - const accessorMock: ElementValueAccessor = { + const accessorMock: ElementsValueAccessor = { get: () => undefined, set: jest.fn(), }; @@ -203,7 +203,7 @@ describe('uncontrolledPlugin', () => { }); test('multiple elements are passed to set accessor', () => { - const accessorMock: ElementValueAccessor = { + const accessorMock: ElementsValueAccessor = { get: () => 'xxx', set: jest.fn(), }; @@ -225,7 +225,7 @@ describe('uncontrolledPlugin', () => { }); test('non-connected elements are ignored', () => { - const accessorMock: ElementValueAccessor = { + const accessorMock: ElementsValueAccessor = { get: () => 'xxx', set: jest.fn(), }; From ab81085d82528ddf7e592aee2b6c365ff36fa7f6 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sun, 12 Nov 2023 22:23:15 +0300 Subject: [PATCH 30/33] Better callOrGet signature --- packages/react/src/main/useField.ts | 2 +- packages/roqueform/src/main/utils.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index 5e89af23..b2ccc8df 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -39,5 +39,5 @@ export function useField( export function useField(initialValue?: unknown, plugin?: PluginInjector) { const accessor = useContext(AccessorContext); - return (useRef().current ||= createField(callOrGet(initialValue, undefined), plugin!, accessor)); + return (useRef().current ||= createField(callOrGet(initialValue), plugin!, accessor)); } diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index ee717234..e7a4f266 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -11,6 +11,15 @@ export function isEqual(a: unknown, b: unknown): boolean { return a === b || (a !== a && b !== b); } +/** + * If value is a function then it is called, otherwise the value is returned as is. + * + * @param value The value to return or a callback to call. + * @returns The value or the call result. + * @template T The returned value. + */ +export function callOrGet(value: T | (() => T)): T; + /** * If value is a function then it is called with the given argument, otherwise the value is returned as is. * @@ -20,8 +29,10 @@ export function isEqual(a: unknown, b: unknown): boolean { * @template T The returned value. * @template A The value callback argument. */ -export function callOrGet(value: T | ((arg: A) => T), arg: A): T { - return typeof value === 'function' ? (value as Function)(arg) : value; +export function callOrGet(value: T | ((arg: A) => T), arg: A): T; + +export function callOrGet(value: unknown, arg?: unknown) { + return typeof value !== 'function' ? value : arguments.length === 1 ? value() : value(arg); } /** From ded6f6185a0128eb849c1922622250304e014529 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Sun, 12 Nov 2023 23:58:04 +0300 Subject: [PATCH 31/33] Replaced currentTarget with origin --- .../src/main/constraintValidationPlugin.ts | 19 +- packages/react/src/main/FieldRenderer.ts | 2 +- .../react/src/test/FieldRenderer.test.tsx | 6 +- packages/reset-plugin/src/main/resetPlugin.ts | 19 +- .../reset-plugin/src/test/resetPlugin.test.ts | 15 +- packages/roqueform/src/main/createField.ts | 14 +- packages/roqueform/src/main/typings.ts | 25 ++- packages/roqueform/src/main/utils.ts | 21 +- .../roqueform/src/main/validationPlugin.ts | 16 +- .../roqueform/src/test/createField.test.ts | 61 +++--- .../src/test/validationPlugin.test.tsx | 139 ++++++++++-- .../src/test/scrollToErrorPlugin.test.tsx | 18 +- .../src/main/uncontrolledPlugin.ts | 1 + .../src/test/uncontrolledPlugin.test.ts | 200 +++++++++--------- 14 files changed, 326 insertions(+), 230 deletions(-) diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index ed5c5241..181f1808 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -1,13 +1,4 @@ -import { - createEvent, - dispatchEvents, - Event, - Field, - PluginInjector, - PluginOf, - Subscriber, - Unsubscribe, -} from 'roqueform'; +import { dispatchEvents, Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform'; const EVENT_CHANGE_ERROR = 'change:error'; @@ -105,7 +96,7 @@ export interface ConstraintValidationPlugin { * @see {@link error} * @see {@link isInvalid} */ - on(eventType: 'change:error', subscriber: Subscriber): Unsubscribe; + on(eventType: 'change:error', subscriber: Subscriber, string | null>): Unsubscribe; } /** @@ -221,7 +212,7 @@ function setError( field.error = error; field.errorOrigin = errorOrigin; - events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); + events.push({ type: EVENT_CHANGE_ERROR, target: field, origin: field, data: originalError }); if (originalError !== null) { return events; @@ -249,7 +240,7 @@ function deleteError(field: Field, errorOrigin: 1 | field.errorOrigin = 1; if (originalError !== (field.error = element.validationMessage)) { - events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); + events.push({ type: EVENT_CHANGE_ERROR, target: field, origin: field, data: originalError }); } return events; } @@ -259,7 +250,7 @@ function deleteError(field: Field, errorOrigin: 1 | field.errorOrigin = 0; field.errorCount--; - events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); + events.push({ type: EVENT_CHANGE_ERROR, target: field, origin: field, data: originalError }); for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { ancestor.errorCount--; diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index b1a8de9c..aee8a9c1 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -52,7 +52,7 @@ export function FieldRenderer>( useLayoutEffect( () => field.on('*', event => { - if (eagerlyUpdated || event.target === field) { + if (eagerlyUpdated || event.origin === field) { rerender(); } if (field.isTransient || event.type !== 'change:value' || event.target !== field) { diff --git a/packages/react/src/test/FieldRenderer.test.tsx b/packages/react/src/test/FieldRenderer.test.tsx index 06aecd1b..4cfc0628 100644 --- a/packages/react/src/test/FieldRenderer.test.tsx +++ b/packages/react/src/test/FieldRenderer.test.tsx @@ -43,7 +43,7 @@ describe('FieldRenderer', () => { expect(renderMock).toHaveBeenCalledTimes(1); }); - test('does not re-render if eagerlyUpdated and child field value is changed', async () => { + test('re-renders if eagerlyUpdated and child field value is changed', async () => { const renderMock = jest.fn(); const rootField = createField(); @@ -63,7 +63,7 @@ describe('FieldRenderer', () => { expect(renderMock).toHaveBeenCalledTimes(2); }); - test('triggers onChange handler when value is changed non-transiently', async () => { + test('triggers onChange when value is changed non-transiently', async () => { const handleChangeMock = jest.fn(); const rootField = createField(); @@ -84,7 +84,7 @@ describe('FieldRenderer', () => { expect(handleChangeMock).toHaveBeenNthCalledWith(1, 222); }); - test('triggers onChange handler when value is changed transiently', async () => { + test('does not trigger onChange when value is changed transiently', async () => { const handleChangeMock = jest.fn(); const rootField = createField(); diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index ebfb76c1..accec5b0 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,5 +1,4 @@ import { - createEvent, dispatchEvents, Event, Field, @@ -59,7 +58,7 @@ export interface ResetPlugin { * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: 'change:initialValue', subscriber: Subscriber>): Unsubscribe; + on(eventType: 'change:initialValue', subscriber: Subscriber>): Unsubscribe; } /** @@ -107,22 +106,22 @@ function setInitialValue(field: Field, initialValue: unknown): void } function propagateInitialValue( + origin: Field, target: Field, - field: Field, initialValue: unknown, events: Event[] ): Event[] { - events.unshift(createEvent('change:initialValue', field, initialValue)); + events.push({ type: 'change:initialValue', target, origin, data: target.initialValue }); - field.initialValue = initialValue; + target.initialValue = initialValue; - if (field.children !== null) { - for (const child of field.children) { - const childInitialValue = field.valueAccessor.get(initialValue, child.key); - if (child !== target && isEqual(child.initialValue, childInitialValue)) { + if (target.children !== null) { + for (const child of target.children) { + const childInitialValue = target.valueAccessor.get(initialValue, child.key); + if (child !== origin && isEqual(child.initialValue, childInitialValue)) { continue; } - propagateInitialValue(target, child, childInitialValue, events); + propagateInitialValue(origin, child, childInitialValue, events); } } return events; diff --git a/packages/reset-plugin/src/test/resetPlugin.test.ts b/packages/reset-plugin/src/test/resetPlugin.test.ts index 9262d0e2..90942849 100644 --- a/packages/reset-plugin/src/test/resetPlugin.test.ts +++ b/packages/reset-plugin/src/test/resetPlugin.test.ts @@ -52,7 +52,20 @@ describe('resetPlugin', () => { field.setInitialValue(initialValue2); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'change:initialValue', + target: field, + origin: field, + data: { aaa: 111 }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:initialValue', + target: field.at('aaa'), + origin: field, + data: 111, + }); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(field.at('aaa').initialValue).toBe(222); expect(field.at('aaa').isDirty).toBe(true); diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 43b4e4b4..56a2b75b 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,5 +1,5 @@ import { Event, Field, PluginInjector, Subscriber, ValueAccessor } from './typings'; -import { callOrGet, createEvent, dispatchEvents, isEqual } from './utils'; +import { callOrGet, dispatchEvents, isEqual } from './utils'; import { naturalAccessor } from './naturalAccessor'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types @@ -131,18 +131,18 @@ function setValue(field: Field, value: unknown, transient: boolean): void { dispatchEvents(propagateValue(field, changeRoot, value, [])); } -function propagateValue(origin: Field, field: Field, value: unknown, events: Event[]): Event[] { - events.unshift(createEvent('change:value', field, value)); +function propagateValue(origin: Field, target: Field, value: unknown, events: Event[]): Event[] { + events.push({ type: 'change:value', target, origin, data: target.value }); - field.value = value; + target.value = value; - if (field.children !== null) { - for (const child of field.children) { + if (target.children !== null) { + for (const child of target.children) { if (child.isTransient) { continue; } - const childValue = field.valueAccessor.get(value, child.key); + const childValue = target.valueAccessor.get(value, child.key); if (child !== origin && isEqual(child.value, childValue)) { continue; } diff --git a/packages/roqueform/src/main/typings.ts b/packages/roqueform/src/main/typings.ts index 3a677277..864a0987 100644 --- a/packages/roqueform/src/main/typings.ts +++ b/packages/roqueform/src/main/typings.ts @@ -10,24 +10,27 @@ export type Field = FieldController { +export interface Event { /** * The type of the event. */ type: string; /** - * The field to which the event subscriber has been added. + * The field onto which the event was dispatched. Usually, this is the field which has changed. */ - currentTarget: CurrentTarget; + target: Field; /** - * The field onto which the event was dispatched. + * The field that caused this event to be dispatched onto {@link target the target field}. + * + * For example: if a child field value is set, then the parent field value to updated as well. For all events + * dispatched in this scenario, the origin is the child field. */ - target: Field>; + origin: Field; /** * The {@link type type-specific} data related to the {@link target target field}. @@ -39,10 +42,10 @@ export interface Event { * The callback that receives events dispatched by {@link Field a field}. * * @param event The dispatched event. - * @template CurrentTarget The field where the event is dispatched. + * @template Plugin The plugin injected into the field. * @template Data The additional data related to the event. */ -export type Subscriber = (event: Event) => void; +export type Subscriber = (event: Event) => void; /** * Unsubscribes the subscriber. No-op if subscriber was already unsubscribed. @@ -141,7 +144,7 @@ export interface FieldController { * @see {@link on} * @protected */ - ['subscribers']: Record[] | undefined> | null; + ['subscribers']: Record[] | undefined> | null; /** * The accessor that reads the field value from the value of the parent fields, and updates parent value. @@ -201,7 +204,7 @@ export interface FieldController { * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: '*', subscriber: Subscriber): Unsubscribe; + on(eventType: '*', subscriber: Subscriber): Unsubscribe; /** * Subscribes to {@link value the field value} changes. {@link Event.data} contains the previous field value. @@ -210,7 +213,7 @@ export interface FieldController { * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: 'change:value', subscriber: Subscriber>): Unsubscribe; + on(eventType: 'change:value', subscriber: Subscriber): Unsubscribe; } /** diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index e7a4f266..54a7f06b 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,4 +1,4 @@ -import { Event, FieldController } from './typings'; +import { Event } from './typings'; /** * [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison operation. @@ -35,21 +35,6 @@ export function callOrGet(value: unknown, arg?: unknown) { return typeof value !== 'function' ? value : arguments.length === 1 ? value() : value(arg); } -/** - * Creates the new event that would be dispatched from target field. - * - * @param type The type of the event. - * @param target The target field where the event is dispatched. - * @param data The data carried by the event. - */ -export function createEvent, Data>( - type: string, - target: Target, - data: Data -): Event { - return { type, currentTarget: target, target, data }; -} - /** * Dispatches multiple events to field subscribers. * @@ -57,15 +42,13 @@ export function createEvent, Data>( */ export function dispatchEvents(events: readonly Event[]): void { for (const event of events) { - for (let field = event.currentTarget; field !== null; field = field.parent) { + for (let field = event.target; field !== null; field = field.parent) { const { subscribers } = field; if (subscribers === null) { continue; } - event.currentTarget = field; - const typeSubscribers = subscribers[event.type]; const globSubscribers = subscribers['*']; diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 338ace7f..aba888eb 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -1,5 +1,5 @@ import { Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from './typings'; -import { createEvent, dispatchEvents, isEqual } from './utils'; +import { dispatchEvents, isEqual } from './utils'; const EVENT_CHANGE_ERROR = 'change:error'; const ERROR_ABORT = 'Validation aborted'; @@ -137,7 +137,7 @@ export interface ValidationPlugin { * @see {@link error} * @see {@link isInvalid} */ - on(eventType: 'change:error', subscriber: Subscriber): Unsubscribe; + on(eventType: 'change:error', subscriber: Subscriber, Error | null>): Unsubscribe; /** * Subscribes to the start of the validation. {@link Event.data} carries the validation that is going to start. @@ -148,7 +148,7 @@ export interface ValidationPlugin { * @see {@link validation} * @see {@link isValidating} */ - on(eventType: 'validation:start', subscriber: Subscriber>>): Unsubscribe; + on(eventType: 'validation:start', subscriber: Subscriber, Validation>>): Unsubscribe; /** * Subscribes to the end of the validation. Check {@link isInvalid} to detect the actual validity status. @@ -160,7 +160,7 @@ export interface ValidationPlugin { * @see {@link validation} * @see {@link isValidating} */ - on(eventType: 'validation:end', subscriber: Subscriber>>): Unsubscribe; + on(eventType: 'validation:end', subscriber: Subscriber, Validation>>): Unsubscribe; /** * Associates a validation error with the field and notifies the subscribers. Use this method in @@ -294,7 +294,7 @@ function setError(field: Field, error: unknown, errorOrigin: 1 field.error = error; field.errorOrigin = errorOrigin; - events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); + events.push({ type: EVENT_CHANGE_ERROR, target: field, origin: field, data: originalError }); if (originalError !== null) { return events; @@ -319,7 +319,7 @@ function deleteError(field: Field, errorOrigin: 1 | 2, events: field.errorOrigin = 0; field.errorCount--; - events.push(createEvent(EVENT_CHANGE_ERROR, field, originalError)); + events.push({ type: EVENT_CHANGE_ERROR, target: field, origin: field, data: originalError }); for (let ancestor = field.parent; ancestor !== null; ancestor = ancestor.parent) { ancestor.errorCount--; @@ -344,7 +344,7 @@ function clearErrors(field: Field, errorOrigin: 1 | 2, events: function startValidation(field: Field, validation: Validation, events: Event[]): Event[] { field.validation = validation; - events.push(createEvent('validation:start', field, validation)); + events.push({ type: 'validation:start', target: field, origin: validation.root, data: validation }); if (field.children !== null) { for (const child of field.children) { @@ -363,7 +363,7 @@ function endValidation(field: Field, validation: Validation, e field.validation = null; - events.push(createEvent('validation:end', field, validation)); + events.push({ type: 'validation:end', target: field, origin: validation.root, data: validation }); if (field.children !== null) { for (const child of field.children) { diff --git a/packages/roqueform/src/test/createField.test.ts b/packages/roqueform/src/test/createField.test.ts index f1df5cd6..096f7a63 100644 --- a/packages/roqueform/src/test/createField.test.ts +++ b/packages/roqueform/src/test/createField.test.ts @@ -79,29 +79,35 @@ describe('createField', () => { }); test('calls a glob subscriber when value is updated', () => { - const rootSubscriberMock = jest.fn(); + const subscriberMock = jest.fn(); const aaaSubscriberMock = jest.fn(); const field = createField({ aaa: 111 }); - field.on('*', rootSubscriberMock); + field.on('*', subscriberMock); field.at('aaa').on('*', aaaSubscriberMock); field.at('aaa').setValue(222); - expect(rootSubscriberMock).toHaveBeenCalledTimes(1); - expect(rootSubscriberMock).toHaveBeenNthCalledWith(1, { + expect(subscriberMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { type: 'change:value', - origin: field.at('aaa'), target: field, + origin: field.at('aaa'), data: { aaa: 111 }, }); + expect(subscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:value', + target: field.at('aaa'), + origin: field.at('aaa'), + data: 111, + }); expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { type: 'change:value', - origin: field.at('aaa'), target: field.at('aaa'), + origin: field.at('aaa'), data: 111, }); }); @@ -117,19 +123,25 @@ describe('createField', () => { field.at('aaa').setValue(222); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(subscriberMock).toHaveBeenNthCalledWith(1, { type: 'change:value', - origin: field.at('aaa'), target: field, + origin: field.at('aaa'), data: { aaa: 111 }, }); + expect(subscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:value', + target: field.at('aaa'), + origin: field.at('aaa'), + data: 111, + }); expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { type: 'change:value', - origin: field.at('aaa'), target: field.at('aaa'), + origin: field.at('aaa'), data: 111, }); }); @@ -150,8 +162,8 @@ describe('createField', () => { expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { type: 'change:value', - origin: field.at('aaa'), target: field.at('aaa'), + origin: field.at('aaa'), data: 111, }); }); @@ -168,14 +180,7 @@ describe('createField', () => { field.setValue({ aaa: 222, bbb: 'aaa' }); expect(bbbSubscriberMock).not.toHaveBeenCalled(); - expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); - expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { - type: 'change:value', - origin: field, - target: field.at('aaa'), - data: 111, - }); }); test('sets a value to a root field', () => { @@ -224,15 +229,8 @@ describe('createField', () => { field.at('aaa').on('*', aaaSubscriberMock); field.at('aaa').setTransientValue(222); - expect(subscriberMock).toHaveBeenCalledTimes(0); - + expect(subscriberMock).toHaveBeenCalledTimes(1); expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); - expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { - type: 'change:value', - origin: field.at('aaa'), - target: field.at('aaa'), - data: 111, - }); }); test('does not leave fields in an inconsistent state if a subscriber throws an error', () => { @@ -270,7 +268,7 @@ describe('createField', () => { const nextValue = { aaa: 333 }; field.setValue(nextValue); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(field.value).toBe(nextValue); @@ -292,7 +290,7 @@ describe('createField', () => { field.setValue({ aaa: 333 }); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(field.at('aaa').value).toBe(222); @@ -316,8 +314,8 @@ describe('createField', () => { expect(subscriberMock).toHaveBeenCalledTimes(1); expect(subscriberMock).toHaveBeenNthCalledWith(1, { type: 'change:value', - origin: field, target: field, + origin: field, data: initialValue, }); @@ -347,14 +345,13 @@ describe('createField', () => { test('an actual parent value is visible in the child field subscriber', done => { const field = createField({ aaa: 111 }); - const newValue = { aaa: 222 }; field.at('aaa').on('*', event => { - expect(event.target.value).toBe(newValue); + expect(event.target.value).toBe(222); done(); }); - field.setValue(newValue); + field.setValue({ aaa: 222 }); }); test('does not cache a child field for which the plugin has thrown an error', () => { @@ -400,6 +397,6 @@ describe('createField', () => { field.at('aaa').setValue(222); expect(field.value.aaa).toBe(333); - expect(subscriberMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(4); }); }); diff --git a/packages/roqueform/src/test/validationPlugin.test.tsx b/packages/roqueform/src/test/validationPlugin.test.tsx index 691a7502..0f33d572 100644 --- a/packages/roqueform/src/test/validationPlugin.test.tsx +++ b/packages/roqueform/src/test/validationPlugin.test.tsx @@ -151,7 +151,7 @@ describe('validationPlugin', () => { expect(field.at('bbb').isInvalid).toBe(false); expect(field.at('bbb').error).toBeNull(); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(3); expect(aaaSubscriberMock).toHaveBeenCalledTimes(1); expect(bbbSubscriberMock).toHaveBeenCalledTimes(2); }); @@ -181,7 +181,7 @@ describe('validationPlugin', () => { expect(field.at('bbb').isInvalid).toBe(false); expect(field.at('bbb').error).toBeNull(); - expect(subscriberMock).toHaveBeenCalledTimes(2); + expect(subscriberMock).toHaveBeenCalledTimes(4); expect(aaaSubscriberMock).toHaveBeenCalledTimes(2); expect(bbbSubscriberMock).toHaveBeenCalledTimes(2); }); @@ -239,27 +239,57 @@ describe('validationPlugin', () => { expect(field.at('aaa').isInvalid).toBe(true); expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(3); + expect(subscriberMock).toHaveBeenCalledTimes(5); expect(subscriberMock).toHaveBeenNthCalledWith(1, { type: 'validation:start', - origin: field, target: field, - data: undefined, + origin: field, + data: { root: field, abortController: null }, }); expect(subscriberMock).toHaveBeenNthCalledWith(2, { + type: 'validation:start', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: null }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(3, { type: 'change:error', + target: field.at('aaa'), origin: field.at('aaa'), - target: field, data: null, }); - expect(subscriberMock).toHaveBeenNthCalledWith(3, { + expect(subscriberMock).toHaveBeenNthCalledWith(4, { type: 'validation:end', - origin: field, target: field, - data: undefined, + origin: field, + data: { root: field, abortController: null }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(5, { + type: 'validation:end', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: null }, }); expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'validation:start', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: null }, + }); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:error', + target: field.at('aaa'), + origin: field.at('aaa'), + data: null, + }); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(3, { + type: 'validation:end', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: null }, + }); }); test('synchronously validates the root field with a callback validator', () => { @@ -286,7 +316,7 @@ describe('validationPlugin', () => { expect(field.at('aaa').isInvalid).toBe(true); expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(3); + expect(subscriberMock).toHaveBeenCalledTimes(5); expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); @@ -316,15 +346,45 @@ describe('validationPlugin', () => { expect(field.at('aaa').isInvalid).toBe(true); expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(3); expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'validation:start', + target: field.at('aaa'), + origin: field.at('aaa'), + data: { root: field.at('aaa'), abortController: null }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(2, { type: 'change:error', + target: field.at('aaa'), origin: field.at('aaa'), - target: field, data: null, }); + expect(subscriberMock).toHaveBeenNthCalledWith(3, { + type: 'validation:end', + target: field.at('aaa'), + origin: field.at('aaa'), + data: { root: field.at('aaa'), abortController: null }, + }); expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'validation:start', + target: field.at('aaa'), + origin: field.at('aaa'), + data: { root: field.at('aaa'), abortController: null }, + }); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:error', + target: field.at('aaa'), + origin: field.at('aaa'), + data: null, + }); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(3, { + type: 'validation:end', + target: field.at('aaa'), + origin: field.at('aaa'), + data: { root: field.at('aaa'), abortController: null }, + }); }); test('synchronously validates multiple fields', () => { @@ -468,8 +528,57 @@ describe('validationPlugin', () => { expect(field.at('aaa').isInvalid).toBe(true); expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(3); + expect(subscriberMock).toHaveBeenCalledTimes(5); + expect(subscriberMock).toHaveBeenNthCalledWith(1, { + type: 'validation:start', + target: field, + origin: field, + data: { root: field, abortController: expect.any(AbortController) }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(2, { + type: 'validation:start', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: expect.any(AbortController) }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(3, { + type: 'change:error', + target: field.at('aaa'), + origin: field.at('aaa'), + data: null, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(4, { + type: 'validation:end', + target: field, + origin: field, + data: { root: field, abortController: expect.any(AbortController) }, + }); + expect(subscriberMock).toHaveBeenNthCalledWith(5, { + type: 'validation:end', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: expect.any(AbortController) }, + }); + expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, { + type: 'validation:start', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: expect.any(AbortController) }, + }); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(2, { + type: 'change:error', + target: field.at('aaa'), + origin: field.at('aaa'), + data: null, + }); + expect(aaaSubscriberMock).toHaveBeenNthCalledWith(3, { + type: 'validation:end', + target: field.at('aaa'), + origin: field, + data: { root: field, abortController: expect.any(AbortController) }, + }); }); test('asynchronously validates the child field', async () => { @@ -505,7 +614,7 @@ describe('validationPlugin', () => { expect(field.at('aaa').isInvalid).toBe(true); expect(field.at('aaa').error).toBe(222); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(3); expect(aaaSubscriberMock).toHaveBeenCalledTimes(3); }); @@ -675,6 +784,6 @@ describe('validationPlugin', () => { field.at('aaa').setValue(222); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx index 1a40e429..85629e89 100644 --- a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx +++ b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx @@ -24,7 +24,7 @@ class DOMRect { } describe('scrollToErrorPlugin', () => { - test('returns false if there are no errors', () => { + test('returns null if there are no errors', () => { const field = createField( { aaa: 111 }, composePlugins( @@ -33,7 +33,7 @@ describe('scrollToErrorPlugin', () => { ) ); - expect(field.scrollToError()).toBe(false); + expect(field.scrollToError()).toBe(null); }); test('scrolls to error at index with RTL text direction', async () => { @@ -63,49 +63,49 @@ describe('scrollToErrorPlugin', () => { }); // Scroll to default index - rootField.scrollToError(); + expect(rootField.scrollToError()).toBe(rootField.at('aaa')); expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); aaaScrollIntoViewMock.mockClear(); bbbScrollIntoViewMock.mockClear(); // Scroll to 0 - rootField.scrollToError(0); + expect(rootField.scrollToError(0)).toBe(rootField.at('aaa')); expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); aaaScrollIntoViewMock.mockClear(); bbbScrollIntoViewMock.mockClear(); // Scroll to 1 - rootField.scrollToError(1); + expect(rootField.scrollToError(1)).toBe(rootField.at('bbb')); expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); expect(bbbScrollIntoViewMock).toHaveBeenCalledTimes(1); aaaScrollIntoViewMock.mockClear(); bbbScrollIntoViewMock.mockClear(); // Scroll to 2 - rootField.scrollToError(2); + expect(rootField.scrollToError(2)).toBe(null); expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); aaaScrollIntoViewMock.mockClear(); bbbScrollIntoViewMock.mockClear(); // Scroll to -1 - rootField.scrollToError(1); + expect(rootField.scrollToError(-1)).toBe(rootField.at('bbb')); expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); expect(bbbScrollIntoViewMock).toHaveBeenCalledTimes(1); aaaScrollIntoViewMock.mockClear(); bbbScrollIntoViewMock.mockClear(); // Scroll to -2 - rootField.scrollToError(-2); + expect(rootField.scrollToError(-2)).toBe(rootField.at('aaa')); expect(aaaScrollIntoViewMock).toHaveBeenCalledTimes(1); expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); aaaScrollIntoViewMock.mockClear(); bbbScrollIntoViewMock.mockClear(); // Scroll to -3 - rootField.scrollToError(-3); + expect(rootField.scrollToError(-3)).toBe(null); expect(aaaScrollIntoViewMock).not.toHaveBeenCalled(); expect(bbbScrollIntoViewMock).not.toHaveBeenCalled(); aaaScrollIntoViewMock.mockClear(); diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index 106b0251..1b1978ce 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -53,6 +53,7 @@ export interface UncontrolledPlugin { */ export function uncontrolledPlugin(accessor = elementsValueAccessor): PluginInjector { return field => { + field.element = null; field.elements = []; field.elementsMap = new Map(); field.elementsValueAccessor = accessor; diff --git a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts index be4124af..d2ac27cd 100644 --- a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts +++ b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts @@ -20,18 +20,18 @@ describe('uncontrolledPlugin', () => { element.type = 'number'; field.on('*', subscriberMock); - field.at('aaa').observe(element); + field.at('aaa').ref(element); - fireEvent.change(element, { target: { value: '222' } }); + fireEvent.input(element, { target: { value: '222' } }); - expect(subscriberMock).toHaveBeenCalledTimes(1); + expect(subscriberMock).toHaveBeenCalledTimes(2); expect(field.value).toEqual({ aaa: 222 }); }); test('updates input value on field change', () => { const field = createField({ aaa: 111 }, uncontrolledPlugin()); - field.at('aaa').observe(element); + field.at('aaa').ref(element); field.at('aaa').setValue(222); expect(element.value).toBe('222'); @@ -42,7 +42,7 @@ describe('uncontrolledPlugin', () => { element.type = 'number'; - field.at('aaa').observe(element); + field.at('aaa').ref(element); expect(element.value).toBe('111'); }); @@ -60,114 +60,114 @@ describe('uncontrolledPlugin', () => { expect(refMock).not.toHaveBeenCalled(); - field.at('aaa').observe(element); + field.at('aaa').ref(element); expect(refMock).toHaveBeenCalledTimes(1); expect(refMock).toHaveBeenNthCalledWith(1, element); }); - test('does not invoke preceding plugin if an additional element is added', () => { - const refMock = jest.fn(); - const plugin = (field: any) => { - field.ref = refMock; - }; - - const element1 = document.body.appendChild(document.createElement('input')); - const element2 = document.body.appendChild(document.createElement('input')); - - const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - - field.at('aaa').observe(element1); - field.at('aaa').observe(element2); - - expect(refMock).toHaveBeenCalledTimes(1); - expect(refMock).toHaveBeenNthCalledWith(1, element1); - }); - - test('invokes preceding plugin if the head element has changed', done => { - const refMock = jest.fn(); - const plugin = (field: any) => { - field.ref = refMock; - }; - - const element1 = document.body.appendChild(document.createElement('input')); - const element2 = document.body.appendChild(document.createElement('textarea')); - - const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - - field.at('aaa').observe(element1); - field.at('aaa').observe(element2); - - expect(refMock).toHaveBeenCalledTimes(1); - expect(refMock).toHaveBeenNthCalledWith(1, element1); - - element1.remove(); - - queueMicrotask(() => { - expect(refMock).toHaveBeenCalledTimes(2); - expect(refMock).toHaveBeenNthCalledWith(2, element2); - done(); - }); - }); - - test('invokes preceding plugin if the head element was removed', done => { - const refMock = jest.fn(); - const plugin = (field: any) => { - field.ref = refMock; - }; - - const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - - field.at('aaa').observe(element); - - element.remove(); - - queueMicrotask(() => { - expect(refMock).toHaveBeenCalledTimes(2); - expect(refMock).toHaveBeenNthCalledWith(1, element); - expect(refMock).toHaveBeenNthCalledWith(2, null); - done(); - }); - }); - - test('null refs are not propagated to preceding plugin', () => { - const refMock = jest.fn(); - const plugin = (field: any) => { - field.ref = refMock; - }; - - const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - - field.at('aaa').observe(null); - - expect(refMock).not.toHaveBeenCalled(); - }); + // test('does not invoke preceding plugin if an additional element is added', () => { + // const refMock = jest.fn(); + // const plugin = (field: any) => { + // field.ref = refMock; + // }; + // + // const element1 = document.body.appendChild(document.createElement('input')); + // const element2 = document.body.appendChild(document.createElement('input')); + // + // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); + // + // field.at('aaa').refFor(1)(element1); + // field.at('aaa').refFor(2)(element2); + // + // expect(refMock).toHaveBeenCalledTimes(1); + // expect(refMock).toHaveBeenNthCalledWith(1, element1); + // }); + // + // test('invokes preceding plugin if the head element has changed', done => { + // const refMock = jest.fn(); + // const plugin = (field: any) => { + // field.ref = refMock; + // }; + // + // const element1 = document.body.appendChild(document.createElement('input')); + // const element2 = document.body.appendChild(document.createElement('textarea')); + // + // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); + // + // field.at('aaa').ref(element1); + // field.at('aaa').ref(element2); + // + // expect(refMock).toHaveBeenCalledTimes(1); + // expect(refMock).toHaveBeenNthCalledWith(1, element1); + // + // element1.remove(); + // + // queueMicrotask(() => { + // expect(refMock).toHaveBeenCalledTimes(2); + // expect(refMock).toHaveBeenNthCalledWith(2, element2); + // done(); + // }); + // }); + // + // test('invokes preceding plugin if the head element was removed', done => { + // const refMock = jest.fn(); + // const plugin = (field: any) => { + // field.ref = refMock; + // }; + // + // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); + // + // field.at('aaa').ref(element); + // + // element.remove(); + // + // queueMicrotask(() => { + // expect(refMock).toHaveBeenCalledTimes(2); + // expect(refMock).toHaveBeenNthCalledWith(1, element); + // expect(refMock).toHaveBeenNthCalledWith(2, null); + // done(); + // }); + // }); + // + // test('null refs are not propagated to preceding plugin', () => { + // const refMock = jest.fn(); + // const plugin = (field: any) => { + // field.ref = refMock; + // }; + // + // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); + // + // field.at('aaa').ref(null); + // + // expect(refMock).not.toHaveBeenCalled(); + // }); test('does not call setValue if the same value multiple times', () => { - const accessorMock: ElementsValueAccessor = { + const elementsValueAccessorMock: ElementsValueAccessor = { get: jest.fn(() => 'xxx'), set: jest.fn(), }; - const field = createField('aaa', uncontrolledPlugin(accessorMock)); + const field = createField('aaa', uncontrolledPlugin(elementsValueAccessorMock)); - const setValueMock = (field.setValue = jest.fn(field.setValue)); + const setValueSpy = jest.spyOn(field, 'setValue'); - field.observe(element); + field.ref(element); - expect(accessorMock.set).toHaveBeenCalledTimes(1); - expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); - expect(accessorMock.get).not.toHaveBeenCalled(); - expect(setValueMock).not.toHaveBeenCalled(); + expect(elementsValueAccessorMock.set).toHaveBeenCalledTimes(1); + expect(elementsValueAccessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); + expect(elementsValueAccessorMock.get).not.toHaveBeenCalled(); + expect(setValueSpy).not.toHaveBeenCalled(); fireEvent.change(element, { target: { value: 'bbb' } }); fireEvent.input(element, { target: { value: 'bbb' } }); - expect(setValueMock).toHaveBeenCalledTimes(1); - expect(setValueMock).toHaveBeenNthCalledWith(1, 'xxx'); + expect(setValueSpy).toHaveBeenCalledTimes(1); + expect(setValueSpy).toHaveBeenNthCalledWith(1, 'xxx'); - expect(accessorMock.set).toHaveBeenCalledTimes(2); - expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element], 'xxx'); + expect(elementsValueAccessorMock.set).toHaveBeenCalledTimes(2); + expect(elementsValueAccessorMock.set).toHaveBeenNthCalledWith(2, [element], 'xxx'); }); test('uses accessor to set values to the element', () => { @@ -178,7 +178,7 @@ describe('uncontrolledPlugin', () => { const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('aaa').observe(element); + field.at('aaa').ref(element); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element], 111); @@ -213,12 +213,12 @@ describe('uncontrolledPlugin', () => { const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('aaa').observe(element1); + field.at('aaa').ref(element1); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element1], 111); - field.at('aaa').observe(element2); + field.at('aaa').ref(element2); expect(accessorMock.set).toHaveBeenCalledTimes(2); expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element1, element2], 111); @@ -234,17 +234,17 @@ describe('uncontrolledPlugin', () => { const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('aaa').observe(element); + field.at('aaa').ref(element); expect(accessorMock.set).toHaveBeenCalledTimes(0); }); - test('mutation observer disconnects after last element is removed', done => { + test('mutation refr disconnects after last element is removed', done => { const disconnectMock = jest.spyOn(MutationObserver.prototype, 'disconnect'); const field = createField({ aaa: 111 }, uncontrolledPlugin()); - field.at('aaa').observe(element); + field.at('aaa').ref(element); element.remove(); From 056efcb75c31d140941622c2ac583222dbecf824 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Mon, 13 Nov 2023 00:01:43 +0300 Subject: [PATCH 32/33] Fixed tests --- package-lock.json | 5811 ++++++++++++++++- .../src/test/uncontrolledPlugin.test.ts | 297 +- 2 files changed, 5659 insertions(+), 449 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5614fdc..c64c7015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "roqueform", - "lockfileVersion": 3, + "lockfileVersion": 2, "requires": true, "packages": { "": { @@ -134,31 +134,31 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", - "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", + "@babel/generator": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.22.20", - "@babel/helpers": "^7.22.15", - "@babel/parser": "^7.22.16", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.20", - "@babel/types": "^7.22.19", - "convert-source-map": "^1.7.0", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", @@ -172,19 +172,13 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.3", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -219,13 +213,13 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -256,9 +250,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", - "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -335,14 +329,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", - "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -434,9 +428,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -506,9 +500,9 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -608,9 +602,9 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -623,9 +617,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -649,19 +643,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", - "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", + "@babel/generator": "^7.23.3", "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.19", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -670,13 +664,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1159,9 +1153,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1214,9 +1208,9 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", - "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -1230,7 +1224,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" + "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -1239,9 +1233,9 @@ } }, "node_modules/@rollup/plugin-typescript": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.3.tgz", - "integrity": "sha512-8o6cNgN44kQBcpsUJTbTXMTtb87oR1O0zgP3Dxm71hrNgparap3VujgofEilTYJo+ivf2ke6uy3/E5QEaiRlDA==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz", + "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -1251,7 +1245,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.14.0||^3.0.0", + "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, @@ -1265,9 +1259,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", - "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -1278,7 +1272,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -1362,9 +1356,9 @@ } }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.0.tgz", + "integrity": "sha512-hcvfZEEyO0xQoZeHmUbuMs7APJCGELpilL7bY+BaJaMP57aWc6q1etFwScnoZDheYjk4ESdlzPdQ33IbsKAK/A==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -1398,15 +1392,15 @@ } }, "node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true }, "node_modules/@types/babel__core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", - "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz", + "integrity": "sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -1417,18 +1411,18 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", - "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", + "version": "7.6.7", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", + "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", - "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -1436,9 +1430,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", - "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", + "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" @@ -1455,48 +1449,48 @@ } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { - "version": "29.5.5", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", - "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -1547,39 +1541,42 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, "node_modules/@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", - "dev": true + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.6", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.6.tgz", - "integrity": "sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg==", + "version": "15.7.10", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", + "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", "dev": true }, "node_modules/@types/react": { - "version": "18.2.22", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz", - "integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==", + "version": "18.2.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", + "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -1588,9 +1585,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", - "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", + "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", "dev": true, "dependencies": { "@types/react": "*" @@ -1603,36 +1600,36 @@ "dev": true }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", + "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==", "dev": true }, "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, "node_modules/@types/tough-cookie": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", - "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", + "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==", "dev": true, "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, "node_modules/abab": { @@ -1642,9 +1639,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1664,9 +1661,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", "dev": true, "engines": { "node": ">=0.4.0" @@ -1945,9 +1942,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, "funding": [ { @@ -1964,10 +1961,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -2016,13 +2013,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2064,9 +2062,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001538", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", - "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", "dev": true, "funding": [ { @@ -2109,9 +2107,9 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -2352,15 +2350,15 @@ } }, "node_modules/deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", + "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", @@ -2370,11 +2368,14 @@ "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", + "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2390,9 +2391,9 @@ } }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dev": true, "dependencies": { "get-intrinsic": "^1.2.1", @@ -2478,9 +2479,9 @@ } }, "node_modules/doubter": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/doubter/-/doubter-3.0.2.tgz", - "integrity": "sha512-JShcLruZqD2taSus9+UUH8K+CxJJitpmoIpSlLziMl6Mj+3l6m+i03LV1x9GLaM0udqjdZZFHCdNp+LDhkZGRA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/doubter/-/doubter-3.0.3.tgz", + "integrity": "sha512-fCOzeNxKhHLPz6L1Nwb3mf0/gp9EMcx9NFhQpJv/mpUSieZdPlGeskeEELR5H/yuXoNp6IewK6DoQkhqWHsfZA==", "peer": true }, "node_modules/eastasianwidth": { @@ -2490,9 +2491,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.524", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.524.tgz", - "integrity": "sha512-iTmhuiGXYo29QoFXwwXbxhAKiDRZQzme6wYVaZNoitg9h1iRaMGu3vNvDyk+gqu5ETK1D6ug9PC5GVS7kSURuw==", + "version": "1.4.581", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.581.tgz", + "integrity": "sha512-6uhqWBIapTJUxgPTCHH9sqdbxIMPt7oXl0VcAL1kOtlU6aECdcMncCrX5Z7sHQ/invtrC9jUQUef7+HhO8vVFw==", "dev": true }, "node_modules/emittery": { @@ -2709,12 +2710,13 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "peer": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2848,10 +2850,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -2881,15 +2886,15 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3004,18 +3009,6 @@ "node": ">=6" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3035,12 +3028,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3085,6 +3078,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -3244,13 +3249,13 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -3358,12 +3363,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3618,18 +3623,18 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", - "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", "dev": true, "dependencies": { "@babel/core": "^7.12.3", @@ -3717,9 +3722,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -4840,9 +4845,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", - "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -5079,9 +5084,9 @@ } }, "node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -5187,9 +5192,9 @@ "dev": true }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5393,14 +5398,50 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", + "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, "engines": { "node": "14 || >=16.14" } }, + "node_modules/path-scurry/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path-scurry/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path-scurry/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5525,18 +5566,18 @@ "dev": true }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/pure-rand": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.3.tgz", - "integrity": "sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", "dev": true, "funding": [ { @@ -5745,9 +5786,9 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -5802,15 +5843,15 @@ } }, "node_modules/rimraf": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", - "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, "dependencies": { - "glob": "^10.2.5" + "glob": "^10.3.7" }, "bin": { - "rimraf": "dist/cjs/src/bin.js" + "rimraf": "dist/esm/bin.mjs" }, "engines": { "node": ">=14" @@ -5829,19 +5870,19 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", + "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5866,9 +5907,9 @@ } }, "node_modules/rollup": { - "version": "3.29.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz", - "integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -5882,15 +5923,15 @@ } }, "node_modules/rollup-plugin-dts": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.0.2.tgz", - "integrity": "sha512-GYCCy9DyE5csSuUObktJBpjNpW2iLZMabNDIiAqzQWBl7l/WHzjvtAXevf8Lftk8EA920tuxeB/g8dM8MVMR6A==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz", + "integrity": "sha512-ijSCPICkRMDKDLBK9torss07+8dl9UpY9z1N/zTeA1cIqdzMlpkV3MOOC7zukyvQfDyxa1s3Dl2+DeiP/G6DOw==", "dev": true, "dependencies": { - "magic-string": "^0.30.3" + "magic-string": "^0.30.4" }, "engines": { - "node": ">=v16" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/Swatinem" @@ -5899,7 +5940,7 @@ "@babel/code-frame": "^7.22.13" }, "peerDependencies": { - "rollup": "^3.25", + "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, @@ -5967,6 +6008,21 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-function-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", @@ -6003,9 +6059,9 @@ } }, "node_modules/shiki": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz", - "integrity": "sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", "dev": true, "dependencies": { "ansi-sequence-parser": "^1.1.0", @@ -6095,9 +6151,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/sprintf-js": { @@ -6484,9 +6540,9 @@ } }, "node_modules/typedoc": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz", - "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", + "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", "dev": true, "dependencies": { "lunr": "^2.3.9", @@ -6505,7 +6561,7 @@ } }, "node_modules/typedoc-custom-css": { - "resolved": "git+ssh://git@github.com/smikhalevski/typedoc-custom-css.git#01bc6410675e95f229b23a17647018b7b54e10e6", + "resolved": "git+ssh://git@github.com/smikhalevski/typedoc-custom-css.git#e1d997c1ef8bc64ac3f215e97f5f7ac520678d90", "dev": true, "license": "MIT" }, @@ -6546,6 +6602,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -6556,9 +6618,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -6596,25 +6658,19 @@ } }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -6748,13 +6804,13 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" @@ -6911,9 +6967,9 @@ } }, "node_modules/zod": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", - "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -6978,9 +7034,6 @@ "name": "@roqueform/uncontrolled-plugin", "version": "1.0.1", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, "peerDependencies": { "roqueform": "^4.0.0" } @@ -6994,5 +7047,5147 @@ "zod": "^3.19.1" } } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/compat-data": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "dev": true + }, + "@babel/core": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "requires": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + } + }, + "@rollup/plugin-typescript": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz", + "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "resolve": "^1.22.1" + } + }, + "@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@roqueform/constraint-validation-plugin": { + "version": "file:packages/constraint-validation-plugin", + "requires": {} + }, + "@roqueform/doubter-plugin": { + "version": "file:packages/doubter-plugin", + "requires": {} + }, + "@roqueform/react": { + "version": "file:packages/react", + "requires": {} + }, + "@roqueform/ref-plugin": { + "version": "file:packages/ref-plugin", + "requires": {} + }, + "@roqueform/reset-plugin": { + "version": "file:packages/reset-plugin", + "requires": {} + }, + "@roqueform/scroll-to-error-plugin": { + "version": "file:packages/scroll-to-error-plugin", + "requires": {} + }, + "@roqueform/uncontrolled-plugin": { + "version": "file:packages/uncontrolled-plugin", + "requires": {} + }, + "@roqueform/zod-plugin": { + "version": "file:packages/zod-plugin", + "requires": {} + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + } + }, + "@testing-library/react": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.0.tgz", + "integrity": "sha512-hcvfZEEyO0xQoZeHmUbuMs7APJCGELpilL7bY+BaJaMP57aWc6q1etFwScnoZDheYjk4ESdlzPdQ33IbsKAK/A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, + "@tsd/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-VtjHPAKJqLJoHHKBDNofzvQB2+ZVxjXU/Gw6INAS9aINLQYVsxfzrQ2s84huCeYWZRTtrr7R0J7XgpZHjNwBCw==", + "dev": true + }, + "@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz", + "integrity": "sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.7", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", + "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", + "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "@types/node": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.10", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", + "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", + "dev": true + }, + "@types/react": { + "version": "18.2.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", + "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", + "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "@types/scheduler": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", + "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", + "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true + }, + "acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "requires": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true + } + } + }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "requires": {} + }, + "deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + } + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "requires": { + "webidl-conversions": "^7.0.0" + } + }, + "doubter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/doubter/-/doubter-3.0.3.tgz", + "integrity": "sha512-fCOzeNxKhHLPz6L1Nwb3mf0/gp9EMcx9NFhQpJv/mpUSieZdPlGeskeEELR5H/yuXoNp6IewK6DoQkhqWHsfZA==", + "peer": true + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.581", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.581.tgz", + "integrity": "sha512-6uhqWBIapTJUxgPTCHH9sqdbxIMPt7oXl0VcAL1kOtlU6aECdcMncCrX5Z7sHQ/invtrC9jUQUef7+HhO8vVFw==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "eslint-formatter-pretty": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", + "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "dev": true, + "requires": { + "@types/eslint": "^7.2.13", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + } + }, + "eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "peer": true + }, + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true + }, + "magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + }, + "meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "dependencies": { + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", + "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "requires": { + "irregular-plurals": "^3.2.0" + } + }, + "prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "requires": { + "glob": "^10.3.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-dts": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz", + "integrity": "sha512-ijSCPICkRMDKDLBK9torss07+8dl9UpY9z1N/zTeA1cIqdzMlpkV3MOOC7zukyvQfDyxa1s3Dl2+DeiP/G6DOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "magic-string": "^0.30.4" + } + }, + "roqueform": { + "version": "file:packages/roqueform" + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shiki": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", + "dev": true, + "requires": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "requires": { + "internal-slot": "^1.0.4" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true + }, + "ts-jest": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "tsd": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.29.0.tgz", + "integrity": "sha512-5B7jbTj+XLMg6rb9sXRBGwzv7h8KJlGOkTHxY63eWpZJiQ5vJbXEjL0u7JkIxwi5EsrRE1kRVUWmy6buK/ii8A==", + "dev": true, + "requires": { + "@tsd/typescript": "~5.2.2", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "jest-diff": "^29.0.3", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "typedoc": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", + "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "typedoc-custom-css": { + "version": "git+ssh://git@github.com/smikhalevski/typedoc-custom-css.git#e1d997c1ef8bc64ac3f215e97f5f7ac520678d90", + "dev": true, + "from": "typedoc-custom-css@github:smikhalevski/typedoc-custom-css#master" + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "v8-to-istanbul": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "requires": {} + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "peer": true + } } } diff --git a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts index d2ac27cd..1e3e986f 100644 --- a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts +++ b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts @@ -13,7 +13,128 @@ describe('uncontrolledPlugin', () => { element.remove(); }); - test('updates field value on input change', () => { + test('invokes ref from the preceding plugin', () => { + const refMock = jest.fn(); + const pluginMock = jest.fn(field => { + field.ref = refMock; + }); + + const field = createField({ aaa: 111 }, composePlugins(pluginMock, uncontrolledPlugin())); + + expect(pluginMock).toHaveBeenCalledTimes(1); + expect(pluginMock).toHaveBeenNthCalledWith(1, field); + + expect(refMock).not.toHaveBeenCalled(); + + field.at('aaa').ref(element); + + expect(refMock).toHaveBeenCalledTimes(1); + expect(refMock).toHaveBeenNthCalledWith(1, element); + }); + + test('ref populates elements', () => { + const field = createField(111, uncontrolledPlugin()); + + field.ref(element); + + expect(field.element).toBe(element); + expect(field.elements).toEqual([element]); + }); + + test('refFor populates elements', () => { + const field = createField(111, uncontrolledPlugin()); + + const element1 = document.createElement('input'); + const element2 = document.createElement('textarea'); + + field.refFor(1)(element1); + field.refFor(2)(element2); + + expect(field.element).toBeNull(); + expect(field.elements).toEqual([element1, element2]); + }); + + test('ref and refFor can be mixed', () => { + const field = createField(111, uncontrolledPlugin()); + + const element1 = document.createElement('input'); + const element2 = document.createElement('textarea'); + + field.ref(element); + field.refFor(1)(element1); + field.refFor(2)(element2); + + expect(field.element).toBe(element); + expect(field.elements).toEqual([element, element1, element2]); + }); + + test('ref and refFor can be called with the same element', () => { + const field = createField(111, uncontrolledPlugin()); + + field.ref(element); + field.refFor(1)(element); + + expect(field.element).toBe(element); + expect(field.elements).toEqual([element]); + }); + + test('refFor can be called with the same element for different keys', () => { + const field = createField(111, uncontrolledPlugin()); + + field.refFor(1)(element); + field.refFor(2)(element); + + expect(field.element).toBeNull(); + expect(field.elements).toEqual([element]); + }); + + test('ref removes an element when called with null', () => { + const field = createField(111, uncontrolledPlugin()); + + const element1 = document.createElement('input'); + const element2 = document.createElement('textarea'); + + field.ref(element); + field.refFor(1)(element1); + field.refFor(2)(element2); + + field.ref(null); + + expect(field.elements).toEqual([element1, element2]); + }); + + test('refFor removes an element when called with null', () => { + const field = createField(111, uncontrolledPlugin()); + + const element1 = document.createElement('input'); + const element2 = document.createElement('textarea'); + + field.refFor(1)(element1); + field.refFor(2)(element2); + + field.refFor(1)(null); + + expect(field.elements).toEqual([element2]); + + field.refFor(2)(null); + + expect(field.elements).toEqual([]); + }); + + test('removed element does not update the field', () => { + const field = createField(111, uncontrolledPlugin()); + const setValueSpy = jest.spyOn(field, 'setValue'); + + field.ref(element); + field.ref(null); + + fireEvent.input(element, { target: { value: '222' } }); + + expect(setValueSpy).not.toHaveBeenCalled(); + expect(field.value).toBe(111); + }); + + test('updates field value when input value changes', () => { const subscriberMock = jest.fn(); const field = createField({ aaa: 111 }, uncontrolledPlugin()); @@ -28,7 +149,7 @@ describe('uncontrolledPlugin', () => { expect(field.value).toEqual({ aaa: 222 }); }); - test('updates input value on field change', () => { + test('updates input value when field value changes', () => { const field = createField({ aaa: 111 }, uncontrolledPlugin()); field.at('aaa').ref(element); @@ -47,127 +168,51 @@ describe('uncontrolledPlugin', () => { expect(element.value).toBe('111'); }); - test('invokes ref from the preceding plugin', () => { - const refMock = jest.fn(); - const pluginMock = jest.fn(field => { - field.ref = refMock; - }); + test('input element uses input event', () => { + const field = createField('aaa', uncontrolledPlugin()); + const setValueSpy = jest.spyOn(field, 'setValue'); - const field = createField({ aaa: 111 }, composePlugins(pluginMock, uncontrolledPlugin())); + field.ref(element); - expect(pluginMock).toHaveBeenCalledTimes(1); - expect(pluginMock).toHaveBeenNthCalledWith(1, field); + fireEvent.input(element, { target: { value: 'bbb' } }); - expect(refMock).not.toHaveBeenCalled(); + expect(setValueSpy).toHaveBeenCalledTimes(1); - field.at('aaa').ref(element); + fireEvent.change(element, { target: { value: 'ccc' } }); - expect(refMock).toHaveBeenCalledTimes(1); - expect(refMock).toHaveBeenNthCalledWith(1, element); + expect(setValueSpy).toHaveBeenCalledTimes(1); }); - // test('does not invoke preceding plugin if an additional element is added', () => { - // const refMock = jest.fn(); - // const plugin = (field: any) => { - // field.ref = refMock; - // }; - // - // const element1 = document.body.appendChild(document.createElement('input')); - // const element2 = document.body.appendChild(document.createElement('input')); - // - // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - // - // field.at('aaa').refFor(1)(element1); - // field.at('aaa').refFor(2)(element2); - // - // expect(refMock).toHaveBeenCalledTimes(1); - // expect(refMock).toHaveBeenNthCalledWith(1, element1); - // }); - // - // test('invokes preceding plugin if the head element has changed', done => { - // const refMock = jest.fn(); - // const plugin = (field: any) => { - // field.ref = refMock; - // }; - // - // const element1 = document.body.appendChild(document.createElement('input')); - // const element2 = document.body.appendChild(document.createElement('textarea')); - // - // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - // - // field.at('aaa').ref(element1); - // field.at('aaa').ref(element2); - // - // expect(refMock).toHaveBeenCalledTimes(1); - // expect(refMock).toHaveBeenNthCalledWith(1, element1); - // - // element1.remove(); - // - // queueMicrotask(() => { - // expect(refMock).toHaveBeenCalledTimes(2); - // expect(refMock).toHaveBeenNthCalledWith(2, element2); - // done(); - // }); - // }); - // - // test('invokes preceding plugin if the head element was removed', done => { - // const refMock = jest.fn(); - // const plugin = (field: any) => { - // field.ref = refMock; - // }; - // - // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - // - // field.at('aaa').ref(element); - // - // element.remove(); - // - // queueMicrotask(() => { - // expect(refMock).toHaveBeenCalledTimes(2); - // expect(refMock).toHaveBeenNthCalledWith(1, element); - // expect(refMock).toHaveBeenNthCalledWith(2, null); - // done(); - // }); - // }); - // - // test('null refs are not propagated to preceding plugin', () => { - // const refMock = jest.fn(); - // const plugin = (field: any) => { - // field.ref = refMock; - // }; - // - // const field = createField({ aaa: 111 }, composePlugins(plugin, uncontrolledPlugin())); - // - // field.at('aaa').ref(null); - // - // expect(refMock).not.toHaveBeenCalled(); - // }); - - test('does not call setValue if the same value multiple times', () => { - const elementsValueAccessorMock: ElementsValueAccessor = { - get: jest.fn(() => 'xxx'), - set: jest.fn(), - }; + test('textarea element uses input event', () => { + const field = createField('aaa', uncontrolledPlugin()); + const setValueSpy = jest.spyOn(field, 'setValue'); + const element = document.body.appendChild(document.createElement('textarea')); + + field.ref(element); + + fireEvent.input(element, { target: { value: 'bbb' } }); + + expect(setValueSpy).toHaveBeenCalledTimes(1); + + fireEvent.change(element, { target: { value: 'ccc' } }); - const field = createField('aaa', uncontrolledPlugin(elementsValueAccessorMock)); + expect(setValueSpy).toHaveBeenCalledTimes(1); + }); + test('select element uses input event', () => { + const field = createField('aaa', uncontrolledPlugin()); const setValueSpy = jest.spyOn(field, 'setValue'); + const element = document.body.appendChild(document.createElement('select')); field.ref(element); - expect(elementsValueAccessorMock.set).toHaveBeenCalledTimes(1); - expect(elementsValueAccessorMock.set).toHaveBeenNthCalledWith(1, [element], 'aaa'); - expect(elementsValueAccessorMock.get).not.toHaveBeenCalled(); - expect(setValueSpy).not.toHaveBeenCalled(); - - fireEvent.change(element, { target: { value: 'bbb' } }); fireEvent.input(element, { target: { value: 'bbb' } }); - expect(setValueSpy).toHaveBeenCalledTimes(1); - expect(setValueSpy).toHaveBeenNthCalledWith(1, 'xxx'); + expect(setValueSpy).toHaveBeenCalledTimes(0); + + fireEvent.change(element, { target: { value: 'ccc' } }); - expect(elementsValueAccessorMock.set).toHaveBeenCalledTimes(2); - expect(elementsValueAccessorMock.set).toHaveBeenNthCalledWith(2, [element], 'xxx'); + expect(setValueSpy).toHaveBeenCalledTimes(1); }); test('uses accessor to set values to the element', () => { @@ -208,49 +253,19 @@ describe('uncontrolledPlugin', () => { set: jest.fn(), }; - const element1 = document.body.appendChild(document.createElement('input')); - const element2 = document.body.appendChild(document.createElement('textarea')); + const element1 = document.createElement('input'); + const element2 = document.createElement('textarea'); const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - field.at('aaa').ref(element1); + field.at('aaa').refFor(1)(element1); expect(accessorMock.set).toHaveBeenCalledTimes(1); expect(accessorMock.set).toHaveBeenNthCalledWith(1, [element1], 111); - field.at('aaa').ref(element2); + field.at('aaa').refFor(2)(element2); expect(accessorMock.set).toHaveBeenCalledTimes(2); expect(accessorMock.set).toHaveBeenNthCalledWith(2, [element1, element2], 111); }); - - test('non-connected elements are ignored', () => { - const accessorMock: ElementsValueAccessor = { - get: () => 'xxx', - set: jest.fn(), - }; - - const element = document.createElement('input'); - - const field = createField({ aaa: 111 }, uncontrolledPlugin(accessorMock)); - - field.at('aaa').ref(element); - - expect(accessorMock.set).toHaveBeenCalledTimes(0); - }); - - test('mutation refr disconnects after last element is removed', done => { - const disconnectMock = jest.spyOn(MutationObserver.prototype, 'disconnect'); - - const field = createField({ aaa: 111 }, uncontrolledPlugin()); - - field.at('aaa').ref(element); - - element.remove(); - - queueMicrotask(() => { - expect(disconnectMock).toHaveBeenCalledTimes(1); - done(); - }); - }); }); From 3068d19051fb58f18498150d980001c1c48032e2 Mon Sep 17 00:00:00 2001 From: smikhalevski Date: Mon, 13 Nov 2023 12:36:03 +0300 Subject: [PATCH 33/33] Better naming --- README.md | 8 +- .../src/main/constraintValidationPlugin.ts | 2 +- .../src/test/doubterPlugin.test-d.ts | 4 +- packages/react/README.md | 2 +- packages/react/src/main/AccessorContext.ts | 9 -- packages/react/src/main/FieldRenderer.ts | 2 +- .../react/src/main/ValueAccessorContext.ts | 9 ++ packages/react/src/main/index.ts | 2 +- packages/react/src/main/useField.ts | 4 +- .../react/src/test/FieldRenderer.test.tsx | 22 ++-- packages/reset-plugin/src/main/resetPlugin.ts | 1 + .../src/test/resetPlugin.test-d.ts | 2 +- packages/roqueform/src/main/createField.ts | 4 +- packages/roqueform/src/main/index.ts | 2 +- ...ralAccessor.ts => naturalValueAccessor.ts} | 4 +- .../src/test/composePlugins.test-d.ts | 14 +-- .../roqueform/src/test/createField.test-d.ts | 6 +- .../roqueform/src/test/createField.test.ts | 4 +- .../src/test/naturalAccessor.test.ts | 111 ------------------ .../src/test/naturalValueAccessor.test.ts | 111 ++++++++++++++++++ ...ts => createElementsValueAccessor.test.ts} | 0 .../zod-plugin/src/test/zodPlugin.test-d.ts | 4 +- tsconfig.typedoc.json | 6 - typedoc.json | 3 +- 24 files changed, 165 insertions(+), 171 deletions(-) delete mode 100644 packages/react/src/main/AccessorContext.ts create mode 100644 packages/react/src/main/ValueAccessorContext.ts rename packages/roqueform/src/main/{naturalAccessor.ts => naturalValueAccessor.ts} (94%) delete mode 100644 packages/roqueform/src/test/naturalAccessor.test.ts create mode 100644 packages/roqueform/src/test/naturalValueAccessor.test.ts rename packages/uncontrolled-plugin/src/test/{createElementValueAccessor.test.ts => createElementsValueAccessor.test.ts} (100%) delete mode 100644 tsconfig.typedoc.json diff --git a/README.md b/README.md index 4754f813..ad7e669a 100644 --- a/README.md +++ b/README.md @@ -298,15 +298,15 @@ field values. [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. You can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses -[`naturalAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalAccessor.html): +[`naturalValueAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalValueAccessor.html): ```ts -import { createField, naturalAccessor } from 'roqueform'; +import { createField, naturalValueAccessor } from 'roqueform'; -const field = createField(['Mars', 'Venus'], naturalAccessor); +const field = createField(['Mars', 'Venus'], naturalValueAccessor); ``` -`naturalAccessor` supports plain object, array, `Map`-like, and `Set`-like instances. +`naturalValueAccessor` supports plain object, array, `Map`-like, and `Set`-like instances. If the field value object has `add` and `Symbol.iterator` methods, it is treated as a `Set`: diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 181f1808..14885252 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -38,7 +38,7 @@ export interface ConstraintValidationPlugin { * The origin of the associated error: * - 0 if there's no associated error. * - 1 if an error was set by Constraint Validation API; - * - 2 if an error was set using {@link ValidationPlugin.setError}; + * - 2 if an error was set using {@link ConstraintValidationPlugin.setError}; * * @protected */ diff --git a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts index afd2d4fe..2fd237fc 100644 --- a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts +++ b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts @@ -3,6 +3,6 @@ import { expectType } from 'tsd'; import { createField, Field } from 'roqueform'; import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin'; -const shape = d.object({ foo: d.object({ bar: d.string() }) }); +const shape = d.object({ aaa: d.object({ bbb: d.string() }) }); -expectType>(createField({ foo: { bar: 'aaa' } }, doubterPlugin(shape))); +expectType>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape))); diff --git a/packages/react/README.md b/packages/react/README.md index ac0782e7..a78013c3 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -62,7 +62,7 @@ You can provide the initial value for a field. ```ts useField({ planet: 'Mars' }); -// ⮕ Field<{ foo: string }> +// ⮕ Field<{ planet: string }> ``` If you pass a callback as an initial value, it would be invoked when the field is initialized. diff --git a/packages/react/src/main/AccessorContext.ts b/packages/react/src/main/AccessorContext.ts deleted file mode 100644 index 81155911..00000000 --- a/packages/react/src/main/AccessorContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext, Context } from 'react'; -import { naturalAccessor, ValueAccessor } from 'roqueform'; - -/** - * The context that is used by {@link useField} to retrieve an accessor. - */ -export const AccessorContext: Context = createContext(naturalAccessor); - -AccessorContext.displayName = 'AccessorContext'; diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index aee8a9c1..e01d29cf 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -35,7 +35,7 @@ export interface FieldRendererProps> } /** - * The component that subscribes to the {@link Field} instance and re-renders its children when the field is notified. + * The component that subscribes to the field instance and re-renders its children when the field is notified. * * @template RenderedField The rendered field. */ diff --git a/packages/react/src/main/ValueAccessorContext.ts b/packages/react/src/main/ValueAccessorContext.ts new file mode 100644 index 00000000..580324c3 --- /dev/null +++ b/packages/react/src/main/ValueAccessorContext.ts @@ -0,0 +1,9 @@ +import { Context, createContext } from 'react'; +import { naturalValueAccessor, ValueAccessor } from 'roqueform'; + +/** + * The context that is used by {@link useField} to retrieve a default value accessor. + */ +export const ValueAccessorContext: Context = createContext(naturalValueAccessor); + +ValueAccessorContext.displayName = 'ValueAccessorContext'; diff --git a/packages/react/src/main/index.ts b/packages/react/src/main/index.ts index 72c786b3..39759c99 100644 --- a/packages/react/src/main/index.ts +++ b/packages/react/src/main/index.ts @@ -2,6 +2,6 @@ * @module react */ -export * from './AccessorContext'; +export * from './ValueAccessorContext'; export * from './FieldRenderer'; export * from './useField'; diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index b2ccc8df..fefd8b30 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -1,6 +1,6 @@ import { useContext, useRef } from 'react'; import { callOrGet, createField, Field, PluginInjector } from 'roqueform'; -import { AccessorContext } from './AccessorContext'; +import { ValueAccessorContext } from './ValueAccessorContext'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types type NoInfer = T extends infer T ? T : never; @@ -37,7 +37,7 @@ export function useField( ): Field; export function useField(initialValue?: unknown, plugin?: PluginInjector) { - const accessor = useContext(AccessorContext); + const accessor = useContext(ValueAccessorContext); return (useRef().current ||= createField(callOrGet(initialValue), plugin!, accessor)); } diff --git a/packages/react/src/test/FieldRenderer.test.tsx b/packages/react/src/test/FieldRenderer.test.tsx index 4cfc0628..dd8bd2ed 100644 --- a/packages/react/src/test/FieldRenderer.test.tsx +++ b/packages/react/src/test/FieldRenderer.test.tsx @@ -7,12 +7,12 @@ describe('FieldRenderer', () => { test('passes the field as an argument', () => { render( createElement(() => { - const rootField = useField({ foo: 'bar' }); + const rootField = useField({ aaa: 111 }); return ( - + {field => { - expect(field).toBe(rootField.at('foo')); + expect(field).toBe(rootField.at('aaa')); return null; }} @@ -38,7 +38,7 @@ describe('FieldRenderer', () => { render(createElement(() => {renderMock})); - await act(() => rootField.at('foo').setValue(222)); + await act(() => rootField.at('aaa').setValue(222)); expect(renderMock).toHaveBeenCalledTimes(1); }); @@ -58,7 +58,7 @@ describe('FieldRenderer', () => { )) ); - await act(() => rootField.at('foo').setValue(222)); + await act(() => rootField.at('aaa').setValue(222)); expect(renderMock).toHaveBeenCalledTimes(2); }); @@ -70,7 +70,7 @@ describe('FieldRenderer', () => { render( createElement(() => ( {() => null} @@ -78,7 +78,7 @@ describe('FieldRenderer', () => { )) ); - await act(() => rootField.at('foo').setValue(222)); + await act(() => rootField.at('aaa').setValue(222)); expect(handleChangeMock).toHaveBeenCalledTimes(1); expect(handleChangeMock).toHaveBeenNthCalledWith(1, 222); @@ -91,7 +91,7 @@ describe('FieldRenderer', () => { render( createElement(() => ( {() => null} @@ -100,13 +100,13 @@ describe('FieldRenderer', () => { ); await act(() => { - rootField.at('foo').setTransientValue(222); - rootField.at('foo').setTransientValue(333); + rootField.at('aaa').setTransientValue(222); + rootField.at('aaa').setTransientValue(333); }); expect(handleChangeMock).toHaveBeenCalledTimes(0); - await act(() => rootField.at('foo').propagate()); + await act(() => rootField.at('aaa').propagate()); expect(handleChangeMock).toHaveBeenCalledTimes(1); expect(handleChangeMock).toHaveBeenNthCalledWith(1, 333); diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index accec5b0..c662e5b0 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,3 +1,4 @@ +import type { FieldController } from 'roqueform'; import { dispatchEvents, Event, diff --git a/packages/reset-plugin/src/test/resetPlugin.test-d.ts b/packages/reset-plugin/src/test/resetPlugin.test-d.ts index b8121000..09ec10f1 100644 --- a/packages/reset-plugin/src/test/resetPlugin.test-d.ts +++ b/packages/reset-plugin/src/test/resetPlugin.test-d.ts @@ -2,4 +2,4 @@ import { expectType } from 'tsd'; import { createField } from 'roqueform'; import { resetPlugin } from '@roqueform/reset-plugin'; -expectType(createField({ foo: 111 }, resetPlugin()).at('foo').initialValue); +expectType(createField({ aaa: 111 }, resetPlugin()).at('aaa').initialValue); diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 56a2b75b..ad39d300 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,6 +1,6 @@ import { Event, Field, PluginInjector, Subscriber, ValueAccessor } from './typings'; import { callOrGet, dispatchEvents, isEqual } from './utils'; -import { naturalAccessor } from './naturalAccessor'; +import { naturalValueAccessor } from './naturalValueAccessor'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types type NoInfer = T extends infer T ? T : never; @@ -41,7 +41,7 @@ export function createField(initialValue?: unknown, plugin?: PluginInjector | Va accessor = plugin; plugin = undefined; } - return getOrCreateField(accessor || naturalAccessor, null, null, initialValue, plugin || null); + return getOrCreateField(accessor || naturalValueAccessor, null, null, initialValue, plugin || null); } function getOrCreateField( diff --git a/packages/roqueform/src/main/index.ts b/packages/roqueform/src/main/index.ts index 2beac6db..cdf5678c 100644 --- a/packages/roqueform/src/main/index.ts +++ b/packages/roqueform/src/main/index.ts @@ -4,7 +4,7 @@ export * from './composePlugins'; export * from './createField'; -export * from './naturalAccessor'; +export * from './naturalValueAccessor'; export * from './typings'; export * from './utils'; export * from './validationPlugin'; diff --git a/packages/roqueform/src/main/naturalAccessor.ts b/packages/roqueform/src/main/naturalValueAccessor.ts similarity index 94% rename from packages/roqueform/src/main/naturalAccessor.ts rename to packages/roqueform/src/main/naturalValueAccessor.ts index 5e4a6452..17689715 100644 --- a/packages/roqueform/src/main/naturalAccessor.ts +++ b/packages/roqueform/src/main/naturalValueAccessor.ts @@ -2,9 +2,9 @@ import { ValueAccessor } from './typings'; import { isEqual } from './utils'; /** - * The accessor that reads and writes key-value pairs to well-known object instances. + * The value accessor that reads and writes key-value pairs to well-known object instances. */ -export const naturalAccessor: ValueAccessor = { +export const naturalValueAccessor: ValueAccessor = { get(obj, key) { if (isPrimitive(obj)) { return undefined; diff --git a/packages/roqueform/src/test/composePlugins.test-d.ts b/packages/roqueform/src/test/composePlugins.test-d.ts index df8ebc07..9753e5a4 100644 --- a/packages/roqueform/src/test/composePlugins.test-d.ts +++ b/packages/roqueform/src/test/composePlugins.test-d.ts @@ -1,13 +1,13 @@ import { expectType } from 'tsd'; import { composePlugins, createField, PluginInjector } from 'roqueform'; -declare const plugin1: PluginInjector<{ aaa: number }>; -declare const plugin2: PluginInjector<{ bbb: boolean }>; +declare const plugin1: PluginInjector<{ xxx: number }>; +declare const plugin2: PluginInjector<{ yyy: boolean }>; -expectType>(composePlugins(plugin1, plugin2)); +expectType>(composePlugins(plugin1, plugin2)); -const field = createField({ foo: 111 }, composePlugins(plugin1, plugin2)); +const field = createField({ aaa: 111 }, composePlugins(plugin1, plugin2)); -expectType<{ foo: number }>(field.value); -expectType(field.aaa); -expectType(field.bbb); +expectType<{ aaa: number }>(field.value); +expectType(field.xxx); +expectType(field.yyy); diff --git a/packages/roqueform/src/test/createField.test-d.ts b/packages/roqueform/src/test/createField.test-d.ts index a910ca30..e24a1564 100644 --- a/packages/roqueform/src/test/createField.test-d.ts +++ b/packages/roqueform/src/test/createField.test-d.ts @@ -3,11 +3,11 @@ import { createField } from 'roqueform'; // Optional properties -const field1 = createField<{ foo: { bar?: string } | null }>({ foo: null }); +const field1 = createField<{ aaa: { bbb?: string } | null }>({ aaa: null }); -expectType<{ bar?: string } | null>(field1.at('foo').value); +expectType<{ bbb?: string } | null>(field1.at('aaa').value); -expectType(field1.at('foo').at('bar').value); +expectType(field1.at('aaa').at('bbb').value); // Unions diff --git a/packages/roqueform/src/test/createField.test.ts b/packages/roqueform/src/test/createField.test.ts index 096f7a63..e54e3fef 100644 --- a/packages/roqueform/src/test/createField.test.ts +++ b/packages/roqueform/src/test/createField.test.ts @@ -1,4 +1,4 @@ -import { createField, naturalAccessor } from '../main'; +import { createField, naturalValueAccessor } from '../main'; jest.useFakeTimers(); @@ -19,7 +19,7 @@ describe('createField', () => { expect(field.children).toBeNull(); expect(field.childrenMap).toBeNull(); expect(field.subscribers).toBeNull(); - expect(field.valueAccessor).toBe(naturalAccessor); + expect(field.valueAccessor).toBe(naturalValueAccessor); expect(field.plugin).toBeNull(); }); diff --git a/packages/roqueform/src/test/naturalAccessor.test.ts b/packages/roqueform/src/test/naturalAccessor.test.ts deleted file mode 100644 index de4dc33a..00000000 --- a/packages/roqueform/src/test/naturalAccessor.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { naturalAccessor } from '../main'; - -describe('naturalAccessor', () => { - test('does not read value from primitive values', () => { - expect(naturalAccessor.get(null, 'aaa')).toBeUndefined(); - expect(naturalAccessor.get(undefined, 'aaa')).toBeUndefined(); - expect(naturalAccessor.get(111, 'toString')).toBeUndefined(); - expect(naturalAccessor.get('aaa', 'toString')).toBeUndefined(); - expect(naturalAccessor.get(true, 'toString')).toBeUndefined(); - expect(naturalAccessor.get(() => undefined, 'length')).toBeUndefined(); - expect(naturalAccessor.get(new Date(), 'now')).toBeUndefined(); - expect(naturalAccessor.get(new RegExp(''), 'lastIndex')).toBeUndefined(); - }); - - test('reads value from an array', () => { - expect(naturalAccessor.get([111], 0)).toBe(111); - }); - - test('reads value from an object', () => { - expect(naturalAccessor.get({ aaa: 111 }, 'aaa')).toBe(111); - }); - - test('reads value from a Map', () => { - expect(naturalAccessor.get(new Map().set('aaa', 111), 'aaa')).toBe(111); - }); - - test('reads value from a Set', () => { - expect(naturalAccessor.get(new Set().add(111), 0)).toBe(111); - }); - - test('creates an object', () => { - expect(naturalAccessor.set(null, 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set(undefined, 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set(111, 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set('aaa', 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set(true, 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set(() => undefined, 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set(new Date(), 'aaa', 111)).toEqual({ aaa: 111 }); - expect(naturalAccessor.set(new RegExp(''), 'aaa', 111)).toEqual({ aaa: 111 }); - }); - - test('creates an array', () => { - expect(naturalAccessor.set(null, 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set(undefined, 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set(111, 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set('aaa', 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set(true, 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set(() => undefined, 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set(new Date(), 1, 111)).toEqual([undefined, 111]); - expect(naturalAccessor.set(new RegExp(''), 1, 111)).toEqual([undefined, 111]); - }); - - test('creates an object if key is not an index', () => { - expect(naturalAccessor.set(null, 111.222, 111)).toEqual({ '111.222': 111 }); - }); - - test('creates an object if key is a string', () => { - expect(naturalAccessor.set(null, '111', 'aaa')).toEqual({ 111: 'aaa' }); - }); - - test('clones an array', () => { - const arr = [111]; - const result = naturalAccessor.set(arr, 0, 222); - - expect(result).toEqual([222]); - expect(result).not.toBe(arr); - }); - - test('clones an object', () => { - const obj = { aaa: 111 }; - const result = naturalAccessor.set(obj, 'aaa', 222); - - expect(result).toEqual({ aaa: 222 }); - expect(result).not.toBe(obj); - }); - - test('clones a Map', () => { - const obj = new Map().set('aaa', 111); - const result = naturalAccessor.set(obj, 'aaa', 222); - - expect(result).toEqual(new Map().set('aaa', 222)); - expect(result).not.toBe(obj); - }); - - test('clones a Set', () => { - const obj = new Set([111]); - const result = naturalAccessor.set(obj, 0, 222); - - expect(result).toEqual(new Set([222])); - expect(result).not.toBe(obj); - }); - - test('writes value to a Set at index', () => { - const obj = new Set([111, 222]); - const result = naturalAccessor.set(obj, 1, 333); - - expect(result).toEqual(new Set([111, 333])); - expect(result).not.toBe(obj); - }); - - test('preserves null prototype', () => { - const obj = Object.create(null); - obj.aaa = 111; - - const result = naturalAccessor.set(obj, 'bbb', 222); - - expect(result).toEqual({ aaa: 111, bbb: 222 }); - expect(result).not.toBe(obj); - expect(Object.getPrototypeOf(result)).toBeNull(); - }); -}); diff --git a/packages/roqueform/src/test/naturalValueAccessor.test.ts b/packages/roqueform/src/test/naturalValueAccessor.test.ts new file mode 100644 index 00000000..38012bba --- /dev/null +++ b/packages/roqueform/src/test/naturalValueAccessor.test.ts @@ -0,0 +1,111 @@ +import { naturalValueAccessor } from '../main'; + +describe('naturalValueAccessor', () => { + test('does not read value from primitive values', () => { + expect(naturalValueAccessor.get(null, 'aaa')).toBeUndefined(); + expect(naturalValueAccessor.get(undefined, 'aaa')).toBeUndefined(); + expect(naturalValueAccessor.get(111, 'toString')).toBeUndefined(); + expect(naturalValueAccessor.get('aaa', 'toString')).toBeUndefined(); + expect(naturalValueAccessor.get(true, 'toString')).toBeUndefined(); + expect(naturalValueAccessor.get(() => undefined, 'length')).toBeUndefined(); + expect(naturalValueAccessor.get(new Date(), 'now')).toBeUndefined(); + expect(naturalValueAccessor.get(new RegExp(''), 'lastIndex')).toBeUndefined(); + }); + + test('reads value from an array', () => { + expect(naturalValueAccessor.get([111], 0)).toBe(111); + }); + + test('reads value from an object', () => { + expect(naturalValueAccessor.get({ aaa: 111 }, 'aaa')).toBe(111); + }); + + test('reads value from a Map', () => { + expect(naturalValueAccessor.get(new Map().set('aaa', 111), 'aaa')).toBe(111); + }); + + test('reads value from a Set', () => { + expect(naturalValueAccessor.get(new Set().add(111), 0)).toBe(111); + }); + + test('creates an object', () => { + expect(naturalValueAccessor.set(null, 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set(undefined, 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set(111, 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set('aaa', 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set(true, 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set(() => undefined, 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set(new Date(), 'aaa', 111)).toEqual({ aaa: 111 }); + expect(naturalValueAccessor.set(new RegExp(''), 'aaa', 111)).toEqual({ aaa: 111 }); + }); + + test('creates an array', () => { + expect(naturalValueAccessor.set(null, 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set(undefined, 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set(111, 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set('aaa', 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set(true, 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set(() => undefined, 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set(new Date(), 1, 111)).toEqual([undefined, 111]); + expect(naturalValueAccessor.set(new RegExp(''), 1, 111)).toEqual([undefined, 111]); + }); + + test('creates an object if key is not an index', () => { + expect(naturalValueAccessor.set(null, 111.222, 111)).toEqual({ '111.222': 111 }); + }); + + test('creates an object if key is a string', () => { + expect(naturalValueAccessor.set(null, '111', 'aaa')).toEqual({ 111: 'aaa' }); + }); + + test('clones an array', () => { + const arr = [111]; + const result = naturalValueAccessor.set(arr, 0, 222); + + expect(result).toEqual([222]); + expect(result).not.toBe(arr); + }); + + test('clones an object', () => { + const obj = { aaa: 111 }; + const result = naturalValueAccessor.set(obj, 'aaa', 222); + + expect(result).toEqual({ aaa: 222 }); + expect(result).not.toBe(obj); + }); + + test('clones a Map', () => { + const obj = new Map().set('aaa', 111); + const result = naturalValueAccessor.set(obj, 'aaa', 222); + + expect(result).toEqual(new Map().set('aaa', 222)); + expect(result).not.toBe(obj); + }); + + test('clones a Set', () => { + const obj = new Set([111]); + const result = naturalValueAccessor.set(obj, 0, 222); + + expect(result).toEqual(new Set([222])); + expect(result).not.toBe(obj); + }); + + test('writes value to a Set at index', () => { + const obj = new Set([111, 222]); + const result = naturalValueAccessor.set(obj, 1, 333); + + expect(result).toEqual(new Set([111, 333])); + expect(result).not.toBe(obj); + }); + + test('preserves null prototype', () => { + const obj = Object.create(null); + obj.aaa = 111; + + const result = naturalValueAccessor.set(obj, 'bbb', 222); + + expect(result).toEqual({ aaa: 111, bbb: 222 }); + expect(result).not.toBe(obj); + expect(Object.getPrototypeOf(result)).toBeNull(); + }); +}); diff --git a/packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts b/packages/uncontrolled-plugin/src/test/createElementsValueAccessor.test.ts similarity index 100% rename from packages/uncontrolled-plugin/src/test/createElementValueAccessor.test.ts rename to packages/uncontrolled-plugin/src/test/createElementsValueAccessor.test.ts diff --git a/packages/zod-plugin/src/test/zodPlugin.test-d.ts b/packages/zod-plugin/src/test/zodPlugin.test-d.ts index 3c7a2e1f..10d6c99a 100644 --- a/packages/zod-plugin/src/test/zodPlugin.test-d.ts +++ b/packages/zod-plugin/src/test/zodPlugin.test-d.ts @@ -3,6 +3,6 @@ import { expectType } from 'tsd'; import { createField, Field } from 'roqueform'; import { ZodPlugin, zodPlugin } from '@roqueform/zod-plugin'; -const shape = z.object({ foo: z.object({ bar: z.string() }) }); +const shape = z.object({ aaa: z.object({ bbb: z.string() }) }); -expectType>(createField({ foo: { bar: 'aaa' } }, zodPlugin(shape))); +expectType>(createField({ aaa: { bbb: 'aaa' } }, zodPlugin(shape))); diff --git a/tsconfig.typedoc.json b/tsconfig.typedoc.json deleted file mode 100644 index c77389ea..00000000 --- a/tsconfig.typedoc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": [ - "./packages/*/src/main/**/*" - ] -} diff --git a/typedoc.json b/typedoc.json index 2fc8f2c6..eb9a94e7 100644 --- a/typedoc.json +++ b/typedoc.json @@ -11,7 +11,7 @@ "protected": true, "inherited": true }, - "tsconfig": "./tsconfig.typedoc.json", + "tsconfig": "./tsconfig.json", "customCss": "./node_modules/typedoc-custom-css/custom.css", "entryPoints": [ "./packages/roqueform/src/main/index.ts", @@ -34,7 +34,6 @@ "Promise": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" }, "global": { - "MutationObserver": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/MutationObserver", "Date": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date", "File": "https://developer.mozilla.org/en-US/docs/Web/API/File" }