@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.
@@ -403,7 +403,7 @@ interface FormPresetField {
403
403
  rows?: number;
404
404
  }
405
405
  interface FormPresetDef {
406
- id: string;
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
- export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FORM_PRESETS, FeedbackTiming, FormPresetId, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, 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, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, fieldOptionsOf, firstInvalidField, formPresetById, interpretSubmitResponse, onFieldBlur, onFieldChange, onSubmitAttempt, parseFieldOptions, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
423
- export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormPresetDef, FormPresetField, FormResult, FormRuntimeState, FormSpec, FormTransport, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.29.0",
3
+ "version": "5.30.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -25,7 +25,7 @@ export interface FormPresetField {
25
25
  }
26
26
 
27
27
  export interface FormPresetDef {
28
- id: string;
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
- export const FORM_PRESETS: readonly FormPresetDef[] = Object.freeze([
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: FormPresetId.Contact, name: 'Contact', icon: 'ph-thin ph-envelope-simple',
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: FormPresetId.Newsletter, name: 'Newsletter', icon: 'ph-thin ph-paper-plane-tilt',
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: FormPresetId.Application, name: 'Application', icon: 'ph-thin ph-read-cv-logo',
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: FormPresetId.Booking, name: 'Booking request', icon: 'ph-thin ph-calendar-check',
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
+ }
@@ -19,3 +19,4 @@ export * from './runtime';
19
19
  export * from './rule-catalog';
20
20
  export * from './transport';
21
21
  export * from './form-presets';
22
+ export * from './nesting';