@vectoriox/iox-ui 5.29.0 → 5.30.0
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.
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +83 -6
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +85 -5
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +46 -3
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/forms-core/form-presets.ts +10 -6
- package/src/forms-core/nesting.ts +92 -0
- package/src/forms-core/public-api.ts +1 -0
package/forms-core/index.d.ts
CHANGED
|
@@ -403,7 +403,7 @@ interface FormPresetField {
|
|
|
403
403
|
rows?: number;
|
|
404
404
|
}
|
|
405
405
|
interface FormPresetDef {
|
|
406
|
-
id:
|
|
406
|
+
id: `${FormPresetId}`;
|
|
407
407
|
name: string;
|
|
408
408
|
icon: string;
|
|
409
409
|
/** One line in the panel: what it collects and where it goes. */
|
|
@@ -419,5 +419,48 @@ declare const FORM_PRESETS: readonly FormPresetDef[];
|
|
|
419
419
|
/** The preset with this id, or undefined. */
|
|
420
420
|
declare function formPresetById(id: string | null | undefined): FormPresetDef | undefined;
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
declare enum InteractiveTag {
|
|
423
|
+
Anchor = "a",
|
|
424
|
+
Button = "button"
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Every node type that renders as an interactive element, and the element(s) it can render. A ButtonBlock
|
|
428
|
+
* renders `<a>` or `<button>` by its `tag` trait — both interactive, so switching the tag never lifts the
|
|
429
|
+
* rule. Pinned to the engine components' selectors by `interactive-hosts.spec.ts`.
|
|
430
|
+
*/
|
|
431
|
+
declare const INTERACTIVE_HOST_TAGS: Readonly<Record<string, readonly InteractiveTag[]>>;
|
|
432
|
+
declare const FIELD_GROUP_NODE_TYPE = "FieldGroup";
|
|
433
|
+
declare const SUBMIT_BUTTON_NODE_TYPE = "SubmitButton";
|
|
434
|
+
/**
|
|
435
|
+
* Form content — what a link or a button must never hold: a Form (its controls), a Field (an input, select
|
|
436
|
+
* or textarea), a SubmitButton (a `<button>`) and a FieldGroup (it exists only to hold fields, so it moves as
|
|
437
|
+
* one). A Success / Error block is text and lives only inside its form, so it is not on the list.
|
|
438
|
+
*/
|
|
439
|
+
declare const FORM_CONTENT_TYPES: ReadonlySet<string>;
|
|
440
|
+
/** The minimum a node must look like — a builder ComponentNode and a saved layout node both do. */
|
|
441
|
+
interface NestingNode {
|
|
442
|
+
id?: string;
|
|
443
|
+
type?: string;
|
|
444
|
+
label?: string;
|
|
445
|
+
children?: NestingNode[] | null;
|
|
446
|
+
}
|
|
447
|
+
declare function isInteractiveHost(node: NestingNode | null | undefined): boolean;
|
|
448
|
+
declare function isFormContent(node: NestingNode | null | undefined): boolean;
|
|
449
|
+
/**
|
|
450
|
+
* The first form-content node in `node`'s subtree that sits inside an interactive host, or null. `insideHost`
|
|
451
|
+
* is true when `node` is placed under a host already — so a Form dropped into a link and a link that holds a
|
|
452
|
+
* Form are the same finding.
|
|
453
|
+
*/
|
|
454
|
+
declare function formContentInHost(node: NestingNode | null | undefined, insideHost?: boolean): NestingNode | null;
|
|
455
|
+
/** One place a tree holds form content inside a link or a button. */
|
|
456
|
+
interface MisplacedFormContent<N extends NestingNode = NestingNode> {
|
|
457
|
+
/** The OUTERMOST interactive host holding it. */
|
|
458
|
+
host: N;
|
|
459
|
+
/** A MAXIMAL form-content subtree inside it — the Form, never each of its fields. */
|
|
460
|
+
content: N;
|
|
461
|
+
}
|
|
462
|
+
/** Every form-content subtree a tree holds inside an interactive host, in document order. */
|
|
463
|
+
declare function findFormContentInHosts<N extends NestingNode>(roots: readonly N[] | null | undefined): MisplacedFormContent<N>[];
|
|
464
|
+
|
|
465
|
+
export { ControlType, DEFAULT_EN_MESSAGES, FIELD_GROUP_NODE_TYPE, FIELD_NODE_TYPE, FORM_CONTENT_TYPES, FORM_ERROR_KEYS, FORM_NODE_TYPE, FORM_PRESETS, FeedbackTiming, FormPresetId, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, INTERACTIVE_HOST_TAGS, InteractiveTag, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, SUBMIT_BUTTON_NODE_TYPE, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, fieldOptionsOf, findFormContentInHosts, firstInvalidField, formContentInHost, formPresetById, interpretSubmitResponse, isFormContent, isInteractiveHost, onFieldBlur, onFieldChange, onSubmitAttempt, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
|
|
466
|
+
export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormPresetDef, FormPresetField, FormResult, FormRuntimeState, FormSpec, FormTransport, MisplacedFormContent, NestingNode, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
|
package/index.d.ts
CHANGED
|
@@ -1371,6 +1371,7 @@ declare class IoxComponentRegistryService {
|
|
|
1371
1371
|
private readonly map;
|
|
1372
1372
|
getComponent(type: string): Type<any> | undefined;
|
|
1373
1373
|
hasComponent(type: string): boolean;
|
|
1374
|
+
registeredTypes(): string[];
|
|
1374
1375
|
static ɵfac: i0.ɵɵFactoryDeclaration<IoxComponentRegistryService, never>;
|
|
1375
1376
|
static ɵprov: i0.ɵɵInjectableDeclaration<IoxComponentRegistryService>;
|
|
1376
1377
|
}
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ export interface FormPresetField {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface FormPresetDef {
|
|
28
|
-
id:
|
|
28
|
+
id: `${FormPresetId}`;
|
|
29
29
|
name: string;
|
|
30
30
|
icon: string;
|
|
31
31
|
/** One line in the panel: what it collects and where it goes. */
|
|
@@ -38,9 +38,13 @@ export interface FormPresetDef {
|
|
|
38
38
|
errorText: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// forms-core is imported by both renderers (the field runtime), and a renderer that never reads the presets
|
|
42
|
+
// must drop them: the call is `@__PURE__`, and the ids are written as the enum's VALUES (typed by it, pinned
|
|
43
|
+
// to it by form-presets.spec) — a read of `FormPresetId.X` counts as a side effect to a bundler and kept the
|
|
44
|
+
// whole list in the engine's bundle.
|
|
45
|
+
export const FORM_PRESETS: readonly FormPresetDef[] = /* @__PURE__ */ Object.freeze([
|
|
42
46
|
{
|
|
43
|
-
id:
|
|
47
|
+
id: 'contact', name: 'Contact', icon: 'ph-thin ph-envelope-simple',
|
|
44
48
|
description: 'Name, email and a message. Stored, and emailed to you.',
|
|
45
49
|
formName: 'Contact form',
|
|
46
50
|
fields: [
|
|
@@ -53,7 +57,7 @@ export const FORM_PRESETS: readonly FormPresetDef[] = Object.freeze([
|
|
|
53
57
|
errorText: 'We couldn’t send your message. Please try again — your answers are still here.',
|
|
54
58
|
},
|
|
55
59
|
{
|
|
56
|
-
id:
|
|
60
|
+
id: 'newsletter', name: 'Newsletter', icon: 'ph-thin ph-paper-plane-tilt',
|
|
57
61
|
description: 'One email field plus consent. Stored.',
|
|
58
62
|
formName: 'Newsletter signup',
|
|
59
63
|
fields: [
|
|
@@ -67,7 +71,7 @@ export const FORM_PRESETS: readonly FormPresetDef[] = Object.freeze([
|
|
|
67
71
|
errorText: 'We couldn’t sign you up. Please try again.',
|
|
68
72
|
},
|
|
69
73
|
{
|
|
70
|
-
id:
|
|
74
|
+
id: 'application', name: 'Application', icon: 'ph-thin ph-read-cv-logo',
|
|
71
75
|
description: 'Details plus a CV upload. Stored, with an auto-reply.',
|
|
72
76
|
formName: 'Application',
|
|
73
77
|
fields: [
|
|
@@ -83,7 +87,7 @@ export const FORM_PRESETS: readonly FormPresetDef[] = Object.freeze([
|
|
|
83
87
|
errorText: 'We couldn’t submit your application. Please try again.',
|
|
84
88
|
},
|
|
85
89
|
{
|
|
86
|
-
id:
|
|
90
|
+
id: 'booking', name: 'Booking request', icon: 'ph-thin ph-calendar-check',
|
|
87
91
|
description: 'Date and party size. Stored, and it notifies you.',
|
|
88
92
|
formName: 'Booking request',
|
|
89
93
|
fields: [
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Where form content may NOT render (Doron, 2026-10-20): inside a link or a button.
|
|
2
|
+
//
|
|
3
|
+
// HTML forbids interactive content inside `<a>` and `<button>`, and a click in a field may follow the
|
|
4
|
+
// link. ONE table and ONE walk, shared by the builder's insert rule (refuse it), the builder's audit
|
|
5
|
+
// (find it in a page saved before the rule) and the engine (warn about it, never rewrite it).
|
|
6
|
+
|
|
7
|
+
import { FIELD_NODE_TYPE, FORM_NODE_TYPE } from './collect';
|
|
8
|
+
|
|
9
|
+
export enum InteractiveTag {
|
|
10
|
+
Anchor = 'a',
|
|
11
|
+
Button = 'button',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every node type that renders as an interactive element, and the element(s) it can render. A ButtonBlock
|
|
16
|
+
* renders `<a>` or `<button>` by its `tag` trait — both interactive, so switching the tag never lifts the
|
|
17
|
+
* rule. Pinned to the engine components' selectors by `interactive-hosts.spec.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export const INTERACTIVE_HOST_TAGS: Readonly<Record<string, readonly InteractiveTag[]>> = Object.freeze({
|
|
20
|
+
LinkedContainer: [InteractiveTag.Anchor],
|
|
21
|
+
Link: [InteractiveTag.Anchor],
|
|
22
|
+
Button: [InteractiveTag.Anchor],
|
|
23
|
+
ButtonBlock: [InteractiveTag.Anchor, InteractiveTag.Button],
|
|
24
|
+
SubmitButton: [InteractiveTag.Button],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const FIELD_GROUP_NODE_TYPE = 'FieldGroup';
|
|
28
|
+
export const SUBMIT_BUTTON_NODE_TYPE = 'SubmitButton';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Form content — what a link or a button must never hold: a Form (its controls), a Field (an input, select
|
|
32
|
+
* or textarea), a SubmitButton (a `<button>`) and a FieldGroup (it exists only to hold fields, so it moves as
|
|
33
|
+
* one). A Success / Error block is text and lives only inside its form, so it is not on the list.
|
|
34
|
+
*/
|
|
35
|
+
export const FORM_CONTENT_TYPES: ReadonlySet<string> = new Set([
|
|
36
|
+
FORM_NODE_TYPE, FIELD_NODE_TYPE, FIELD_GROUP_NODE_TYPE, SUBMIT_BUTTON_NODE_TYPE,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** The minimum a node must look like — a builder ComponentNode and a saved layout node both do. */
|
|
40
|
+
export interface NestingNode {
|
|
41
|
+
id?: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
children?: NestingNode[] | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isInteractiveHost(node: NestingNode | null | undefined): boolean {
|
|
48
|
+
return !!node?.type && Object.prototype.hasOwnProperty.call(INTERACTIVE_HOST_TAGS, node.type);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isFormContent(node: NestingNode | null | undefined): boolean {
|
|
52
|
+
return !!node?.type && FORM_CONTENT_TYPES.has(node.type);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The first form-content node in `node`'s subtree that sits inside an interactive host, or null. `insideHost`
|
|
57
|
+
* is true when `node` is placed under a host already — so a Form dropped into a link and a link that holds a
|
|
58
|
+
* Form are the same finding.
|
|
59
|
+
*/
|
|
60
|
+
export function formContentInHost(node: NestingNode | null | undefined, insideHost = false): NestingNode | null {
|
|
61
|
+
if (!node) return null;
|
|
62
|
+
if (insideHost && isFormContent(node)) return node;
|
|
63
|
+
const nowInside = insideHost || isInteractiveHost(node);
|
|
64
|
+
for (const child of node.children ?? []) {
|
|
65
|
+
const found = formContentInHost(child, nowInside);
|
|
66
|
+
if (found) return found;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One place a tree holds form content inside a link or a button. */
|
|
72
|
+
export interface MisplacedFormContent<N extends NestingNode = NestingNode> {
|
|
73
|
+
/** The OUTERMOST interactive host holding it. */
|
|
74
|
+
host: N;
|
|
75
|
+
/** A MAXIMAL form-content subtree inside it — the Form, never each of its fields. */
|
|
76
|
+
content: N;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Every form-content subtree a tree holds inside an interactive host, in document order. */
|
|
80
|
+
export function findFormContentInHosts<N extends NestingNode>(roots: readonly N[] | null | undefined): MisplacedFormContent<N>[] {
|
|
81
|
+
const out: MisplacedFormContent<N>[] = [];
|
|
82
|
+
const visit = (node: N, host: N | null): void => {
|
|
83
|
+
if (host && isFormContent(node)) {
|
|
84
|
+
out.push({ host, content: node });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const nextHost = host ?? (isInteractiveHost(node) ? node : null);
|
|
88
|
+
for (const child of (node.children ?? []) as N[]) visit(child, nextHost);
|
|
89
|
+
};
|
|
90
|
+
for (const root of roots ?? []) visit(root, null);
|
|
91
|
+
return out;
|
|
92
|
+
}
|