@dmitryvim/form-builder 0.5.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,7 @@
1
1
  import type { ContainerElement, RenderContext, ComponentContext, ValidationResult, Element } from "../types/index.js";
2
2
  export declare function setRenderElement(fn: (element: any, ctx: RenderContext) => HTMLElement): void;
3
3
  export declare function renderSingleContainerElement(element: ContainerElement, ctx: RenderContext, wrapper: HTMLElement, pathKey: string): void;
4
- export declare function renderMultipleContainerElement(element: ContainerElement, ctx: RenderContext, wrapper: HTMLElement, _pathKey: string): void;
5
- export declare function setValidateElement(fn: (element: Element, ctx: {
6
- path: string;
7
- }, customScopeRoot?: HTMLElement | null) => {
8
- value: any;
9
- spread: boolean;
10
- skip?: boolean;
11
- }): void;
4
+ export declare function renderMultipleContainerElement(element: ContainerElement, ctx: RenderContext, wrapper: HTMLElement, pathKey: string): void;
12
5
  /**
13
6
  * Validate container field and return extracted value with errors
14
7
  */
@@ -7,10 +7,9 @@ import { renderSwitcherElement, renderMultipleSwitcherElement } from "./switcher
7
7
  import { renderFileElement, renderFilesElement, renderMultipleFileElement } from "./file.js";
8
8
  import { renderColourElement, renderMultipleColourElement } from "./colour.js";
9
9
  import { renderSliderElement, renderMultipleSliderElement } from "./slider.js";
10
- import { renderSingleContainerElement, renderMultipleContainerElement, setValidateElement as setContainerValidateElement } from "./container.js";
10
+ import { renderSingleContainerElement, renderMultipleContainerElement } from "./container.js";
11
11
  import { renderGroupElement } from "./group.js";
12
12
  import { renderTableElement } from "./table.js";
13
13
  import { renderRichInputElement } from "./richinput.js";
14
14
  export declare function renderElement(element: Element, ctx: RenderContext): HTMLElement;
15
- export { setContainerValidateElement };
16
15
  export { renderTextElement, renderMultipleTextElement, renderTextareaElement, renderMultipleTextareaElement, renderNumberElement, renderMultipleNumberElement, renderSelectElement, renderMultipleSelectElement, renderSwitcherElement, renderMultipleSwitcherElement, renderFileElement, renderFilesElement, renderMultipleFileElement, renderColourElement, renderMultipleColourElement, renderSliderElement, renderMultipleSliderElement, renderSingleContainerElement, renderMultipleContainerElement, renderGroupElement, renderTableElement, renderRichInputElement, };
@@ -1,4 +1,4 @@
1
- import type { Schema, ExternalAction, FormData as FormDataResult, Config, State } from "../types/index.js";
1
+ import type { Schema, ExternalAction, FormData as FormDataResult, Config, Locale, State } from "../types/index.js";
2
2
  /**
3
3
  * FormBuilderInstance - Encapsulates all form state and operations
4
4
  * Allows multiple independent forms on the same page without state collisions
@@ -6,6 +6,7 @@ import type { Schema, ExternalAction, FormData as FormDataResult, Config, State
6
6
  export declare class FormBuilderInstance {
7
7
  private state;
8
8
  private instanceId;
9
+ private prefillHintHandler;
9
10
  constructor(config?: Partial<Config>);
10
11
  /**
11
12
  * Get instance ID (useful for debugging and resource prefixing)
@@ -20,7 +21,9 @@ export declare class FormBuilderInstance {
20
21
  */
21
22
  setFormRoot(element: HTMLElement): void;
22
23
  /**
23
- * Configure the form builder
24
+ * Configure the form builder. Translations deep-merge per locale (same as
25
+ * the constructor); a locale without translations — configured or default —
26
+ * is rejected, matching setLocale.
24
27
  */
25
28
  configure(config: Partial<Config>): void;
26
29
  /**
@@ -44,15 +47,33 @@ export declare class FormBuilderInstance {
44
47
  */
45
48
  setMode(mode: "edit" | "readonly"): void;
46
49
  /**
47
- * Set locale
50
+ * Set locale. Custom locales are allowed — their translations must have
51
+ * been provided via the constructor or configure() first.
48
52
  */
49
- setLocale(locale: "en" | "ru"): void;
53
+ setLocale(locale: Locale): void;
50
54
  /**
51
55
  * Trigger onChange callbacks with debouncing
52
56
  * @param fieldPath - Optional field path for field-specific change events
53
- * @param fieldValue - Optional field value for field-specific change events
57
+ * @param fieldValue - Optional field value for field-specific change events.
58
+ * When omitted while fieldPath is given, the value is read from the
59
+ * freshly extracted form data at debounce time — used by structural
60
+ * changes (multi-item add/remove), where the handler has no cheap
61
+ * current value but the array is trivially derivable after the fact.
54
62
  */
55
63
  triggerOnChange(fieldPath?: string, fieldValue?: any): void;
64
+ /**
65
+ * Resolve a DOM field path against the extracted form data.
66
+ *
67
+ * A plain getValueByPath is wrong for paths inside a multiple container:
68
+ * row markers keep gaps after a deletion (`s[2]` may be the first surviving
69
+ * row) while the extracted array is re-packed contiguously — the naive
70
+ * lookup would read a different row, or nothing. Each `[N]` segment is
71
+ * mapped from its marker to the row's position among the container's
72
+ * rendered rows, the same DOM order extraction used to build the array.
73
+ * A bracketed segment that is not a container marker (a multi-value leaf
74
+ * like `tags[1]`, whose indices are contiguous) falls back to the index.
75
+ */
76
+ private resolveDomPathValue;
56
77
  /**
57
78
  * Register an external action that will be displayed as a button
58
79
  * External actions can be form-level (no related_field) or field-level (with related_field)
@@ -122,6 +143,13 @@ export declare class FormBuilderInstance {
122
143
  * Save draft without validation
123
144
  */
124
145
  saveDraft(): FormDataResult;
146
+ /**
147
+ * Post form data to the parent frame — only when the host opted in via
148
+ * `postMessageTarget`. Outside an iframe `window.parent === window`, so an
149
+ * unconditional post broadcast form data and the full schema to any
150
+ * embedding page (targetOrigin "*") on every submit. See CHANGELOG 0.6.0.
151
+ */
152
+ private postToParent;
125
153
  /**
126
154
  * Clear the form - reset all field values to empty while preserving form structure
127
155
  * This is done by re-rendering the form with empty data
@@ -174,4 +202,7 @@ export declare class FormBuilderInstance {
174
202
  */
175
203
  destroy(): void;
176
204
  private disconnectEnableIfObservers;
205
+ private disconnectAutoExpandObservers;
206
+ private removePrefillHintListener;
207
+ private removeTooltipElements;
177
208
  }
@@ -3,6 +3,14 @@ import type { State, Config } from "../types/index.js";
3
3
  * Default configuration for new instances
4
4
  */
5
5
  export declare const defaultConfig: Config;
6
+ /**
7
+ * Merge user-provided translations over a base map, per locale — the base
8
+ * packs survive, and each provided locale merges over the base's same-locale
9
+ * pack. Used by the constructor and configure(); a wholesale replacement
10
+ * (Object.assign) dropped the default en/ru packs and made t() return key
11
+ * names.
12
+ */
13
+ export declare function mergeTranslations(base: Config["translations"], overrides: Partial<Config>["translations"]): Config["translations"];
6
14
  /**
7
15
  * Create a new isolated state object for a FormBuilderInstance
8
16
  * Uses deep merge for translations to preserve default en/ru fallbacks
@@ -15,6 +15,21 @@ export interface ComponentContext {
15
15
  path: string;
16
16
  /** Skip validation checks (for draft mode) */
17
17
  skipValidation?: boolean;
18
+ /**
19
+ * Recursive element validator, threaded per validateForm pass by
20
+ * FormBuilderInstance. Containers use it to validate children with the
21
+ * instance's full context. Threaded through the context — never a module
22
+ * global — so per-instance state cannot leak between instances (the old
23
+ * setContainerValidateElement setter only worked because validateForm is
24
+ * synchronous and re-set it before every pass).
25
+ */
26
+ validateElement?: (element: Element, ctx: {
27
+ path: string;
28
+ }, customScopeRoot?: HTMLElement | null) => {
29
+ value: any;
30
+ spread: boolean;
31
+ skip?: boolean;
32
+ };
18
33
  }
19
34
  /**
20
35
  * Validation result from a component validator
@@ -23,6 +23,7 @@ export interface Translations {
23
23
  openInNewTab: string;
24
24
  changeButton: string;
25
25
  placeholderText: string;
26
+ selectPlaceholder?: string;
26
27
  previewAlt: string;
27
28
  previewUnavailable: string;
28
29
  previewError: string;
@@ -110,6 +111,7 @@ export interface Config {
110
111
  onDownloadError: ((error: Error, resourceId: string, fileName: string) => void) | null;
111
112
  debounceMs: number;
112
113
  verboseErrors: boolean;
114
+ postMessageTarget: string | null;
113
115
  enableFilePreview: boolean;
114
116
  maxPreviewSize: string;
115
117
  readonly: boolean;
@@ -23,4 +23,19 @@ export interface State {
23
23
  * with a torn-down ctx. Reproducible under React StrictMode (double mount).
24
24
  */
25
25
  enableIfObservers: Set<MutationObserver>;
26
+ /**
27
+ * ResizeObservers created by applyAutoExpand (text/textarea/richinput
28
+ * auto-height). Each observer's own callback disconnects it when its
29
+ * textarea has left the DOM — but that check only runs if the observer ever
30
+ * fires again, so a silently removed field held its observer forever.
31
+ * destroy() and every renderForm() disconnect the whole set.
32
+ */
33
+ autoExpandObservers: Set<ResizeObserver>;
34
+ /**
35
+ * Info-button tooltip nodes created by this instance. They live on
36
+ * document.body (position: fixed, moved there so tile overflow can't clip
37
+ * them), so clearing formRoot does not remove them — destroy() and every
38
+ * renderForm() must, or each render cycle leaks its tooltips.
39
+ */
40
+ tooltipElements: Set<HTMLElement>;
26
41
  }
@@ -7,8 +7,13 @@ export declare function isElementReadonly(element: {
7
7
  }): boolean;
8
8
  export declare function isPlainObject(obj: any): obj is Record<string, any>;
9
9
  /**
10
- * Escape HTML special characters to prevent XSS
11
- * Use when inserting user-controlled or translated content via innerHTML
10
+ * Escape HTML special characters to prevent XSS.
11
+ * Use when inserting user-controlled or translated content via innerHTML.
12
+ *
13
+ * Escapes quotes as well as `&<>` because several call sites interpolate
14
+ * into attribute position (e.g. `alt="${escapeHtml(fileName)}"`) — the
15
+ * previous textContent/innerHTML trick left quotes intact, letting a file
16
+ * named `x" onerror="…` break out of the attribute.
12
17
  */
13
18
  export declare function escapeHtml(text: string): string;
14
19
  /**
@@ -42,14 +47,38 @@ export declare function clear(node: HTMLElement): void;
42
47
  export declare function formatFileSize(bytes: number): string;
43
48
  /**
44
49
  * Serialize a value for storage in a hidden input's value attribute.
45
- * Objects/arraysJSON string, null/undefined "", primitives String().
50
+ * undefined"", everything else — null included JSON.
51
+ *
52
+ * JSON for every type — including strings — so deserialization is exact.
53
+ * The previous String() form was lossy: a hidden text field prefilled with
54
+ * "true" or "123" came back from getFormData() as a boolean/number.
55
+ *
56
+ * null serializes as JSON "null" (not "") so that an explicit null survives
57
+ * the round-trip: "" used to be re-read as "unset", which let the hidden
58
+ * validator substitute element.default — updateField(key, null) on a field
59
+ * with a default could never actually produce null.
46
60
  */
47
61
  export declare function serializeHiddenValue(value: any): string;
48
62
  /**
49
63
  * Deserialize a hidden input's value attribute back to its original type.
50
- * Empty string → null, JSON-parseable → parsed value, otherwise → raw string.
64
+ * Empty string → null, JSON → parsed value.
65
+ *
66
+ * The raw-string fallback covers values written into the DOM directly
67
+ * (not through serializeHiddenValue) — hosts poking the input by hand.
51
68
  */
52
69
  export declare function deserializeHiddenValue(raw: string): any;
70
+ /**
71
+ * Read an input's value the way typed extraction reports it — the DOM
72
+ * scrapers feeding enableIf (extractDOMValue, extractRootFormData) must
73
+ * agree with getFormData() for SCALAR fields, or a condition flips on the
74
+ * keystroke and is corrected only by the debounced pass (visible flicker):
75
+ * empty scalar -> null, number/range -> number (rounded per data-decimals),
76
+ * checkbox -> boolean, data-hidden-field -> JSON. Radio stays with the
77
+ * callers (needs a :checked query against a scope). Composite fields (file,
78
+ * table, richinput) extract arrays/objects that no single input carries —
79
+ * the parity guarantee does not extend to them.
80
+ */
81
+ export declare function readTypedInputValue(input: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): any;
53
82
  /**
54
83
  * Create a hidden input element for a hidden form field.
55
84
  */
@@ -1,11 +1,16 @@
1
1
  import type { State } from "../types/index.js";
2
2
  /**
3
- * Strip the `.error-message` node paired with an input by id convention
4
- * (markValidity creates `error-${input.name}` across text/select/slider/
5
- * colour/switcher). Called from updateField paths so a programmatic update
6
- * to a valid value clears any stale error surface left over from the
7
- * previous validation pass — otherwise the field stays visibly "invalid"
8
- * until the next validate cycle.
3
+ * Mark an input's validity: toggle the `invalid` class/title and create,
4
+ * update or remove the paired `.error-message` node. Shared by every scalar
5
+ * component's validator this used to be six near-identical copies, each
6
+ * with its own `error-${Math.random()}` fallback id.
7
+ */
8
+ export declare function markFieldValidity(input: HTMLElement | null, errorMessage: string | null): void;
9
+ /**
10
+ * Strip the `.error-message` node paired with an input. Called from
11
+ * updateField paths so a programmatic update to a valid value clears any
12
+ * stale error surface left over from the previous validation pass —
13
+ * otherwise the field stays visibly "invalid" until the next validate cycle.
9
14
  */
10
15
  export declare function clearFieldError(input: HTMLElement): void;
11
16
  /**
@@ -53,8 +58,9 @@ export declare function ensureThemingHooks(doc: Document): void;
53
58
  * where width hasn't moved — otherwise our own height writes would feed back
54
59
  * into the observer.
55
60
  */
56
- export declare function applyAutoExpand(textarea: HTMLTextAreaElement, options?: {
61
+ export declare function applyAutoExpand(textarea: HTMLTextAreaElement, options: {
57
62
  minRows?: number;
63
+ observers: Set<ResizeObserver>;
58
64
  }): void;
59
65
  /**
60
66
  * Enforce single-line semantics on a textarea: block the Enter key and
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.3",
6
+ "version": "0.6.4",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "./dist/cjs/index.cjs",
9
9
  "module": "./dist/esm/index.js",