@aglyn/aglyn 1.0.0-beta.236 → 1.0.0-beta.238
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/package.json +11 -11
- package/src/lib/app-utils/docs-help-sections.generated.js +9 -6
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +6 -6
- package/src/lib/app-utils/docs-help.generated.js +9 -3
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +246 -22
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +21 -2
- package/src/lib/app-utils/element-ui.js +29 -2
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/foreign-dom-guard.d.ts +17 -0
- package/src/lib/app-utils/foreign-dom-guard.js +66 -0
- package/src/lib/app-utils/foreign-dom-guard.js.map +1 -0
- package/src/lib/app-utils/forms.d.ts +11 -0
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/media-filter.js +5 -1
- package/src/lib/app-utils/media-filter.js.map +1 -1
- package/src/lib/app-utils/media-metadata.d.ts +3 -2
- package/src/lib/app-utils/media-metadata.js +4 -0
- package/src/lib/app-utils/media-metadata.js.map +1 -1
- package/src/lib/app-utils/media-picker-context.d.ts +5 -1
- package/src/lib/app-utils/media-picker-context.js +9 -0
- package/src/lib/app-utils/media-picker-context.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +3 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/release-flags.js +6 -6
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/upload-inspection.js +88 -0
- package/src/lib/app-utils/upload-inspection.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +6 -0
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +7 -0
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +18 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +5 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
|
@@ -25,12 +25,31 @@
|
|
|
25
25
|
import { type ScrollToBehavior } from './site-interactions';
|
|
26
26
|
export * from './element-hidden-style';
|
|
27
27
|
export type ElementVisibilityCommand = 'show' | 'hide' | 'toggle';
|
|
28
|
+
/**
|
|
29
|
+
* The event an element hears when a show/hide/toggle step reaches it
|
|
30
|
+
* (AGL-3717), dispatched ON the element after its hidden class has changed.
|
|
31
|
+
*
|
|
32
|
+
* Most elements need nothing more than the class. One whose "shown" is more
|
|
33
|
+
* than a CSS rule — a lightbox that opens a dialog, a panel that loads its
|
|
34
|
+
* contents only when revealed — subscribes here and does the rest itself, so
|
|
35
|
+
* the interactions system's existing "Show an element" step opens it from any
|
|
36
|
+
* button, link or picture without a step type of its own. Generic on purpose:
|
|
37
|
+
* any element, from any plugin, may answer.
|
|
38
|
+
*/
|
|
39
|
+
export declare const ELEMENT_VISIBILITY_EVENT = "aglyn:element-visibility";
|
|
40
|
+
export interface ElementVisibilityDetail {
|
|
41
|
+
/** Whether the element is hidden now that the step has run. */
|
|
42
|
+
hidden: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Hears visibility steps that reach `element`; returns the unsubscriber. */
|
|
45
|
+
export declare function subscribeElementVisibility(element: EventTarget, handler: (detail: ElementVisibilityDetail) => void): () => void;
|
|
28
46
|
/**
|
|
29
47
|
* Applies a show/hide/toggle command to every element the selector
|
|
30
48
|
* matches. Hide adds the shared hidden class; show removes it AND any
|
|
31
49
|
* inline `display: none` so an element hidden either way reveals; toggle
|
|
32
|
-
* flips per element.
|
|
33
|
-
*
|
|
50
|
+
* flips per element. Each element then hears
|
|
51
|
+
* {@link ELEMENT_VISIBILITY_EVENT}. Returns the number of elements touched
|
|
52
|
+
* (0 for a selector that matches nothing — steps never throw).
|
|
34
53
|
*/
|
|
35
54
|
export declare function applyElementVisibility(command: ElementVisibilityCommand, selector: string, root?: ParentNode): number;
|
|
36
55
|
export interface ElementVisibilityOptions {
|
|
@@ -26,12 +26,32 @@ import { LAYOUT_NODE_ID_PREFIXES } from "./compose-layout-nodes.js";
|
|
|
26
26
|
import { ELEMENT_HIDDEN_CLASS } from "./element-hidden-style.js";
|
|
27
27
|
import { COMPONENT_NODE_ID_PREFIX } from "./reusable-component-keys.js";
|
|
28
28
|
export * from "./element-hidden-style.js";
|
|
29
|
+
/**
|
|
30
|
+
* The event an element hears when a show/hide/toggle step reaches it
|
|
31
|
+
* (AGL-3717), dispatched ON the element after its hidden class has changed.
|
|
32
|
+
*
|
|
33
|
+
* Most elements need nothing more than the class. One whose "shown" is more
|
|
34
|
+
* than a CSS rule — a lightbox that opens a dialog, a panel that loads its
|
|
35
|
+
* contents only when revealed — subscribes here and does the rest itself, so
|
|
36
|
+
* the interactions system's existing "Show an element" step opens it from any
|
|
37
|
+
* button, link or picture without a step type of its own. Generic on purpose:
|
|
38
|
+
* any element, from any plugin, may answer.
|
|
39
|
+
*/ export const ELEMENT_VISIBILITY_EVENT = 'aglyn:element-visibility';
|
|
40
|
+
/** Hears visibility steps that reach `element`; returns the unsubscriber. */ export function subscribeElementVisibility(element, handler) {
|
|
41
|
+
const listener = (event)=>{
|
|
42
|
+
const detail = event.detail;
|
|
43
|
+
if (detail && typeof detail.hidden === 'boolean') handler(detail);
|
|
44
|
+
};
|
|
45
|
+
element.addEventListener(ELEMENT_VISIBILITY_EVENT, listener);
|
|
46
|
+
return ()=>element.removeEventListener(ELEMENT_VISIBILITY_EVENT, listener);
|
|
47
|
+
}
|
|
29
48
|
/**
|
|
30
49
|
* Applies a show/hide/toggle command to every element the selector
|
|
31
50
|
* matches. Hide adds the shared hidden class; show removes it AND any
|
|
32
51
|
* inline `display: none` so an element hidden either way reveals; toggle
|
|
33
|
-
* flips per element.
|
|
34
|
-
*
|
|
52
|
+
* flips per element. Each element then hears
|
|
53
|
+
* {@link ELEMENT_VISIBILITY_EVENT}. Returns the number of elements touched
|
|
54
|
+
* (0 for a selector that matches nothing — steps never throw).
|
|
35
55
|
*/ export function applyElementVisibility(command, selector, root = document) {
|
|
36
56
|
let elements;
|
|
37
57
|
try {
|
|
@@ -49,6 +69,13 @@ export * from "./element-hidden-style.js";
|
|
|
49
69
|
// so "show" always actually reveals.
|
|
50
70
|
if (element.style.display === 'none') element.style.removeProperty('display');
|
|
51
71
|
}
|
|
72
|
+
if (typeof CustomEvent === 'function') {
|
|
73
|
+
element.dispatchEvent(new CustomEvent(ELEMENT_VISIBILITY_EVENT, {
|
|
74
|
+
detail: {
|
|
75
|
+
hidden: nextHidden
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
52
79
|
}
|
|
53
80
|
return elements.length;
|
|
54
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-ui.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Element UI choreography contract (AGL-562): the shared pieces the\n * interactions executor (marketing site runtime), the drawer element\n * (plugins-mui), and the besigner authoring surfaces agree on —\n * show/hide semantics, the drawer command event bus, and the responsive\n * visibility media bands. Pure constants + DOM helpers; every function\n * that touches `window`/`document` is only ever called client-side.\n */\n\nimport { SCROLL_TO_MAX_OFFSET_PX, type ScrollToBehavior } from './site-interactions'\nimport { LAYOUT_NODE_ID_PREFIXES } from './compose-layout-nodes'\nimport { ELEMENT_HIDDEN_CLASS } from './element-hidden-style'\nimport { COMPONENT_NODE_ID_PREFIX } from './reusable-component-keys'\n\nexport * from './element-hidden-style'\n\nexport type ElementVisibilityCommand = 'show' | 'hide' | 'toggle'\n\n/**\n * Applies a show/hide/toggle command to every element the selector\n * matches. Hide adds the shared hidden class; show removes it AND any\n * inline `display: none` so an element hidden either way reveals; toggle\n * flips per element. Returns the number of elements touched (0 for a\n * selector that matches nothing — steps never throw).\n */\nexport function applyElementVisibility(\n command: ElementVisibilityCommand,\n selector: string,\n root: ParentNode = document,\n): number {\n let elements: Element[]\n try {\n elements = Array.from(root.querySelectorAll(selector))\n } catch {\n return 0 // Invalid stored selector — never break the page.\n }\n for (const element of elements) {\n const hidden = element.classList.contains(ELEMENT_HIDDEN_CLASS)\n const nextHidden = command === 'toggle' ? !hidden : command === 'hide'\n element.classList.toggle(ELEMENT_HIDDEN_CLASS, nextHidden)\n if (!nextHidden && element instanceof HTMLElement) {\n // Clear a leftover inline hide (e.g. from custom CSS experiments)\n // so \"show\" always actually reveals.\n if (element.style.display === 'none') element.style.removeProperty('display')\n }\n }\n return elements.length\n}\n\n/* ── Visibility choreography: grace delays + self-dismissal (AGL-589) ── */\n\nexport interface ElementVisibilityOptions {\n /** Defer the change; a later visibility step on the same selector\n * cancels the pending one — the hover grace period. */\n delayMs?: number\n /** Self-dismiss a SHOWN element on Escape / a pointerdown outside it. */\n dismissOn?: Array<'escape' | 'outsideClick'>\n}\n\n// Selector-keyed registries. One document per site runtime, so module\n// scope is the right lifetime; reset exists for tests.\nconst pendingVisibilityTimers = new Map<string, ReturnType<typeof setTimeout>>()\nconst activeDismissers = new Map<string, () => void>()\n\n/** Tears down all pending timers and dismiss listeners (tests). */\nexport function resetElementVisibilityChoreography(): void {\n for (const timer of pendingVisibilityTimers.values()) clearTimeout(timer)\n pendingVisibilityTimers.clear()\n for (const teardown of activeDismissers.values()) teardown()\n activeDismissers.clear()\n}\n\nfunction disarmDismissal(selector: string): void {\n activeDismissers.get(selector)?.()\n activeDismissers.delete(selector)\n}\n\nfunction armDismissal(\n selector: string,\n dismissOn: NonNullable<ElementVisibilityOptions['dismissOn']>,\n doc: Document,\n): void {\n disarmDismissal(selector)\n if (!dismissOn.length) return\n const dismiss = () => {\n applyElementVisibility('hide', selector, doc)\n disarmDismissal(selector)\n }\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') dismiss()\n }\n const onPointerDown = (event: Event) => {\n const target = event.target as Element | null\n // Inside the shown element (or its trigger sharing a wrapper that\n // matches the selector) never dismisses — only genuine outside taps.\n if (target?.closest?.(selector)) return\n dismiss()\n }\n if (dismissOn.includes('escape')) {\n doc.addEventListener('keydown', onKeyDown)\n }\n if (dismissOn.includes('outsideClick')) {\n doc.addEventListener('pointerdown', onPointerDown)\n }\n activeDismissers.set(selector, () => {\n doc.removeEventListener('keydown', onKeyDown)\n doc.removeEventListener('pointerdown', onPointerDown)\n })\n}\n\n/**\n * Runs an element-visibility step with menu-grade choreography (AGL-589):\n *\n * - `delayMs` defers the change; ANY later visibility step on the same\n * selector cancels the pending one first. Hover menus author\n * hover-enter → show (immediate, cancels a pending hide) and\n * hover-leave → hide with a small delay — the classic grace period.\n * - `dismissOn` arms Escape / outside-pointerdown listeners while the\n * element is shown (armed AFTER the change applies, so the click that\n * opened it can never instantly close it) and disarms them on hide.\n *\n * Without options this is exactly {@link applyElementVisibility}.\n */\nexport function runElementVisibilityStep(\n command: ElementVisibilityCommand,\n selector: string,\n options: ElementVisibilityOptions = {},\n doc: Document = document,\n): void {\n // Later intent wins: cancel whatever is pending for this selector.\n const pending = pendingVisibilityTimers.get(selector)\n if (pending != null) {\n clearTimeout(pending)\n pendingVisibilityTimers.delete(selector)\n }\n const apply = () => {\n pendingVisibilityTimers.delete(selector)\n applyElementVisibility(command, selector, doc)\n const dismissOn = options.dismissOn ?? []\n if (!dismissOn.length) return\n // Toggle can land either way — read the DOM for the outcome.\n let shown = command === 'show'\n if (command === 'toggle') {\n try {\n const element = doc.querySelector(selector)\n shown =\n element != null &&\n !element.classList.contains(ELEMENT_HIDDEN_CLASS)\n } catch {\n shown = false\n }\n }\n if (command === 'hide' || !shown) disarmDismissal(selector)\n else armDismissal(selector, dismissOn, doc)\n }\n const delay = Number(options.delayMs)\n if (Number.isFinite(delay) && delay > 0) {\n pendingVisibilityTimers.set(selector, setTimeout(apply, delay))\n } else {\n apply()\n }\n}\n\n/* ── Layout-namespace-insensitive id matching (AGL-573) ─────────────── */\n\n/**\n * Composition namespace prefixes the live runtime can graft onto a node's\n * canvas id. Layout nodes are namespaced during screen composition\n * ({@link LAYOUT_NODE_ID_PREFIXES}) so they can never collide with screen\n * ids — which means an interaction authored against the RAW canvas id\n * (what the besigner builder emits: `menuNodeId` and the\n * `[data-aglyn=\"leaf:<id>\"]` selector) is not byte-equal to the id the\n * live, layout-composed DOM carries.\n *\n * One entry per level of the layout chain (AGL-703): a layout nested\n * inside another carries `layout2__`, and an interaction authored on it\n * must still match. Being a list was already the design; nesting is the\n * case it was left open for.\n */\nconst LEAF_ID_NAMESPACE_PREFIXES = LAYOUT_NODE_ID_PREFIXES\n\n/**\n * Whether an id looks like a reusable-component graft (AGL-1229).\n *\n * A graft's namespace is `cmp__{instanceId}__`, and unlike the layout\n * prefixes it CANNOT be enumerated — there is one per placement, and the\n * instance id is itself often composed: the nav really ships\n * `cmp__layout__52Ef-3t6yd___R91yATrXH`.\n *\n * It also cannot be reliably PARSED off. With a raw id that starts with `_`\n * (nanoid does that routinely) the `___` boundary splits two ways and both\n * are structurally valid, so stripping guesses wrong half the time. Hence\n * {@link leafIdsMatch} compares by suffix instead of normalizing — a\n * composed id always ends in the raw id it was built from.\n */\nconst isComponentGraft = (id: string) =>\n id.startsWith(COMPONENT_NODE_ID_PREFIX)\n\n/**\n * Strips a single leading LAYOUT namespace prefix from a node/leaf id so ids\n * authored against the raw canvas id and ids stamped on the live\n * layout-composed DOM compare equal (AGL-573). Idempotent for an\n * already-raw id, and only a *leading* prefix is removed, so unrelated ids\n * can never be coerced into colliding.\n *\n * Deliberately does NOT try to strip a reusable-component graft — that\n * namespace is unparseable (see {@link isComponentGraft}); `leafIdsMatch`\n * and `expandLeafSelector` handle grafts by suffix.\n *\n * @example\n * normalizeLeafId('layout___5I3TBXywa') // → '_5I3TBXywa'\n * normalizeLeafId('_5I3TBXywa') // → '_5I3TBXywa'\n */\nexport function normalizeLeafId(id: string | undefined | null): string {\n const value = String(id ?? '')\n for (const prefix of LEAF_ID_NAMESPACE_PREFIXES) {\n if (value.startsWith(prefix)) return value.slice(prefix.length)\n }\n return value\n}\n\n/**\n * Whether two node ids address the same node ignoring composition\n * namespace prefixes (AGL-573, extended to reusable-component grafts by\n * AGL-1229) — the durable half of the fix for interactions authored on\n * composed elements, whose stored command id is the raw canvas id while the\n * live element's id is namespaced.\n *\n * Layout prefixes are stripped exactly. A component graft is matched by\n * SUFFIX instead, because its namespace cannot be parsed off unambiguously\n * (see {@link isComponentGraft}) — a composed id always ends in `__` plus\n * the raw id it was built from, and a raw id is a single nanoid with no\n * `__` boundary to spare, so the suffix cannot reach an unrelated node.\n *\n * Two empty ids never match (a missing id is not a wildcard).\n */\nexport function leafIdsMatch(\n a: string | undefined | null,\n b: string | undefined | null,\n): boolean {\n const left = normalizeLeafId(a)\n const right = normalizeLeafId(b)\n if (left === '' || right === '') return false\n if (left === right) return true\n if (isComponentGraft(right) && right.endsWith(`__${left}`)) return true\n if (isComponentGraft(left) && left.endsWith(`__${right}`)) return true\n return false\n}\n\n/**\n * Extracts the leaf id from a canonical `[data-aglyn=\"leaf:<id>\"]`\n * selector (the exact form the renderer and the interaction builder emit),\n * or `undefined` for any other selector.\n */\nexport function leafIdFromSelector(selector: string): string | undefined {\n const match = /^\\s*\\[data-aglyn=\"leaf:(.+)\"\\]\\s*$/.exec(selector)\n return match?.[1] || undefined\n}\n\n/**\n * Rewrites a `[data-aglyn=\"leaf:<id>\"]` selector so it ALSO matches the\n * same node once composition has namespaced its live id (AGL-573, extended\n * to reusable components by AGL-1229):\n *\n * [data-aglyn=\"leaf:_5I3TBXywa\"]\n * → [data-aglyn=\"leaf:_5I3TBXywa\"],\n * [data-aglyn=\"leaf:layout___5I3TBXywa\"],\n * [data-aglyn$=\"___5I3TBXywa\"]\n *\n * The layout alternatives stay enumerated and exactly anchored. The last is\n * a SUFFIX match, and it is the only way to reach a reusable-component\n * graft: its namespace is `cmp__{instanceId}__`, one per placement, so\n * there is no finite prefix list to enumerate.\n *\n * The suffix cannot collide. A composed id always ends in the raw id it was\n * built from, so matching `__` + the exact raw id can only reach a\n * composition OF THAT id — reaching an unrelated node would need that\n * node's own raw id to end in `__` plus a full id, and raw ids are a single\n * fixed-length nanoid with no `__` boundary to spare.\n *\n * Any non-leaf selector — a plain CSS selector an author typed by hand —\n * passes through verbatim.\n */\nexport function expandLeafSelector(selector: string): string {\n const rawId = leafIdFromSelector(selector)\n if (!rawId) return selector\n const base = normalizeLeafId(rawId)\n const exact = [\n base,\n ...LEAF_ID_NAMESPACE_PREFIXES.map((prefix) => `${prefix}${base}`),\n ].map((id) => `[data-aglyn=\"leaf:${id}\"]`)\n return [...exact, `[data-aglyn$=\"__${base}\"]`].join(', ')\n}\n\n/* ── UI command buses (drawer AGL-562, menu AGL-568) ────────────────── */\n\n/**\n * Shared open/close/toggle transport for popup-like elements. Keeping\n * the transport at the DOM level lets the marketing automations engine\n * drive drawers and menus without importing the mui plugin (and vice\n * versa). Each surface keeps its own event name so the drawer contract\n * shipped in AGL-562 stays byte-for-byte stable.\n */\ninterface UiCommandDetail {\n command: 'open' | 'close' | 'toggle'\n nodeId?: string\n}\n\nfunction dispatchUiCommand<TDetail extends UiCommandDetail>(\n eventName: string,\n detail: TDetail,\n): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<TDetail>(eventName, { detail }))\n}\n\nfunction subscribeUiCommands<TDetail extends UiCommandDetail>(\n eventName: string,\n handler: (detail: TDetail) => void,\n): () => void {\n if (typeof window === 'undefined') return () => undefined\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<TDetail>).detail\n if (detail?.command) handler(detail)\n }\n window.addEventListener(eventName, listener)\n return () => window.removeEventListener(eventName, listener)\n}\n\nexport type DrawerCommand = 'open' | 'close' | 'toggle'\n\n/** Window event carrying a drawer command (AGL-562). */\nexport const DRAWER_COMMAND_EVENT = 'aglyn:drawer-command'\n\nexport interface DrawerCommandDetail {\n command: DrawerCommand\n /** Target drawer's canvas node id; absent = the page's first drawer. */\n nodeId?: string\n}\n\n/** Dispatches a drawer command onto the window event bus. */\nexport function dispatchDrawerCommand(\n command: DrawerCommand,\n nodeId?: string,\n): void {\n dispatchUiCommand<DrawerCommandDetail>(DRAWER_COMMAND_EVENT, {\n command,\n ...(nodeId ? { nodeId } : {}),\n })\n}\n\n/**\n * Subscribes to drawer commands; returns the unsubscriber. The handler\n * receives every command — drawer instances filter by their own node id\n * (or first-registered status for broadcasts).\n */\nexport function subscribeDrawerCommands(\n handler: (detail: DrawerCommandDetail) => void,\n): () => void {\n return subscribeUiCommands(DRAWER_COMMAND_EVENT, handler)\n}\n\nexport type MenuCommand = 'open' | 'close' | 'toggle'\n\n/**\n * Window event carrying a nav-menu command (AGL-568): the interactions\n * system's open/close/toggle steps address Dropdown Menu and Mega Menu\n * elements over this bus, exactly like drawers — no bespoke \"open on\"\n * attribute involved.\n */\nexport const MENU_COMMAND_EVENT = 'aglyn:menu-command'\n\nexport interface MenuCommandDetail {\n command: MenuCommand\n /** Target menu's canvas node id; absent = the page's first menu. */\n nodeId?: string\n /**\n * Set when the command came from a hover trigger: a menu opened this\n * way closes itself once the pointer leaves the trigger + panel\n * surface (standard hover-menu UX), while click/command opens stay\n * put until an explicit close, click-away, or Escape.\n */\n hover?: boolean\n}\n\n/** Dispatches a menu command onto the window event bus. */\nexport function dispatchMenuCommand(\n command: MenuCommand,\n nodeId?: string,\n options?: { hover?: boolean },\n): void {\n dispatchUiCommand<MenuCommandDetail>(MENU_COMMAND_EVENT, {\n command,\n ...(nodeId ? { nodeId } : {}),\n ...(options?.hover ? { hover: true } : {}),\n })\n}\n\n/**\n * Subscribes to menu commands; returns the unsubscriber. The handler\n * receives every command — menu instances filter by their own node id\n * (or first-registered status for broadcasts).\n */\nexport function subscribeMenuCommands(\n handler: (detail: MenuCommandDetail) => void,\n): () => void {\n return subscribeUiCommands(MENU_COMMAND_EVENT, handler)\n}\n\n/* ── One-target steps: scroll to an element, play a video (AGL-2867) ── */\n\n/**\n * The element a one-target step acts on: the first match that has a layout\n * box, or null.\n *\n * Every match is read rather than only the first, because one element can\n * legitimately appear twice — an expanded leaf selector reaches each\n * composition of an id, and a page may carry a desktop and a mobile copy with\n * one hidden at each breakpoint. The copy the visitor can see is the one they\n * meant.\n *\n * A match with no box is never acted on. Scrolling to a `display: none`\n * element lands at the top of the page, and pressing a hidden player starts a\n * film nobody can see, so a target that is not on the page in the layout\n * sense makes the step a no-op, as a missing or deleted one does.\n */\nexport function resolveStepTarget(\n selector: string,\n root: ParentNode = document,\n): HTMLElement | SVGElement | null {\n let matches: Element[]\n try {\n matches = Array.from(root.querySelectorAll(selector))\n } catch {\n return null // Invalid stored selector — never break the page.\n }\n const shown = matches.find((element) => element.getClientRects().length > 0)\n return (shown as HTMLElement | SVGElement | undefined) ?? null\n}\n\nexport interface ScrollToStepOptions {\n /** Absent means `smooth`; reduced motion overrides either to `instant`. */\n behavior?: ScrollToBehavior\n /** Pixels left above the target; clamped to 0–{@link SCROLL_TO_MAX_OFFSET_PX}. */\n offsetPx?: number\n}\n\n/** The nearest ancestor that scrolls its own content, below the document. */\nfunction scrollContainerOf(element: Element, view: Window): Element | null {\n const doc = element.ownerDocument\n for (\n let node = element.parentElement;\n node && node !== doc.body && node !== doc.documentElement;\n node = node.parentElement\n ) {\n const { overflowY } = view.getComputedStyle(node)\n if (\n (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&\n node.scrollHeight > node.clientHeight\n ) {\n return node\n }\n }\n return null\n}\n\n/**\n * Focus for a keyboard visitor, without the second scroll `focus()` would\n * start on its own.\n *\n * An element that cannot take focus is given `tabindex=\"-1\"` for the length\n * of the visit to it — focusable by script, never added to the Tab order —\n * and loses it again on blur, so the author's markup is left as it was.\n */\nfunction focusWithoutScrolling(element: HTMLElement | SVGElement): void {\n if (element.tabIndex < 0 && !element.hasAttribute('tabindex')) {\n element.setAttribute('tabindex', '-1')\n element.addEventListener('blur', () => element.removeAttribute('tabindex'), {\n once: true,\n })\n }\n element.focus({ preventScroll: true })\n}\n\n/**\n * Runs a `scrollTo` step: brings the target to the top of the window, less\n * the offset, and moves focus to it. Returns whether there was a target.\n *\n * The position is computed and handed to the window rather than asked of\n * `scrollIntoView`, because the offset has nowhere to go in that call except\n * a `scroll-margin` written onto the author's element. A target inside a box\n * that scrolls its own content is the exception, and there the box has to be\n * scrolled too, which `scrollIntoView` does for every such ancestor at once;\n * the offset is for a header over the page, not for a panel's own edge.\n *\n * Reduced motion is read at the moment of the press, so a visitor who changes\n * the setting mid-visit gets what they asked for on the next one.\n */\nexport function runScrollToStep(\n selector: string,\n options: ScrollToStepOptions = {},\n doc: Document = document,\n): boolean {\n const view = doc.defaultView\n const target = resolveStepTarget(selector, doc)\n if (!view || !target) return false\n const reduceMotion =\n view.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n const behavior: ScrollBehavior =\n reduceMotion || options.behavior === 'instant' ? 'instant' : 'smooth'\n if (scrollContainerOf(target, view)) {\n target.scrollIntoView({ behavior, block: 'start' })\n } else {\n const requested = Number(options.offsetPx)\n const offset = Number.isFinite(requested)\n ? Math.min(Math.max(requested, 0), SCROLL_TO_MAX_OFFSET_PX)\n : 0\n const top = view.scrollY + target.getBoundingClientRect().top - offset\n view.scrollTo({ top: Math.max(0, top), behavior })\n }\n focusWithoutScrolling(target)\n return true\n}\n\n/**\n * The event a Video element answers with its own poster press.\n *\n * Dispatched ON the element, where the drawer and menu commands go to\n * `window`. Those buses need an address because a command with none means\n * \"the page's first drawer\"; a `playVideo` step has already found its element\n * through its selector, so the element that receives the event is the one that\n * should answer, with no id to parse or compare.\n */\nexport const VIDEO_COMMAND_EVENT = 'aglyn:video-command'\n\nexport type VideoCommand = 'play'\n\nexport interface VideoCommandDetail {\n command: VideoCommand\n}\n\n/**\n * Sends a command to an element. Returns whether a Video element answered —\n * a listener registered through {@link subscribeVideoCommands} cancels the\n * event to say so — which is false for an element that is not a Video.\n */\nexport function dispatchVideoCommand(\n element: EventTarget,\n command: VideoCommand,\n): boolean {\n return !element.dispatchEvent(\n new CustomEvent<VideoCommandDetail>(VIDEO_COMMAND_EVENT, {\n detail: { command },\n cancelable: true,\n }),\n )\n}\n\n/** Answers video commands sent to `element`; returns the unsubscriber. */\nexport function subscribeVideoCommands(\n element: EventTarget,\n handler: (detail: VideoCommandDetail) => void,\n): () => void {\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<VideoCommandDetail>).detail\n if (!detail?.command) return\n event.preventDefault()\n handler(detail)\n }\n element.addEventListener(VIDEO_COMMAND_EVENT, listener)\n return () => element.removeEventListener(VIDEO_COMMAND_EVENT, listener)\n}\n\n/**\n * Runs a `playVideo` step: presses the poster of the Video element the\n * selector names. Returns whether a Video answered.\n */\nexport function runPlayVideoStep(\n selector: string,\n doc: Document = document,\n): boolean {\n const target = resolveStepTarget(selector, doc)\n return target ? dispatchVideoCommand(target, 'play') : false\n}\n\n/* ── Responsive visibility bands (AGL-562) ──────────────────────────── */\n\n/**\n * The three authoring-facing visibility bands and the sx media-query\n * keys the styles panel writes `display: none` under. Range-scoped\n * queries (not MUI's mobile-first responsive objects) so hiding one band\n * never needs a \"restore\" display value on the others — the element's\n * natural display simply keeps applying outside the hidden range.\n * Boundaries follow the MUI defaults the device preview uses: mobile\n * under 600, tablet 600–899, desktop 900 and up.\n */\nexport const VISIBILITY_BANDS = ['mobile', 'tablet', 'desktop'] as const\n\nexport type VisibilityBand = (typeof VISIBILITY_BANDS)[number]\n\nexport const VISIBILITY_BAND_MEDIA: Record<VisibilityBand, string> = {\n mobile: '@media (max-width:599.95px)',\n tablet: '@media (min-width:600px) and (max-width:899.95px)',\n desktop: '@media (min-width:900px)',\n}\n"],"names":["SCROLL_TO_MAX_OFFSET_PX","LAYOUT_NODE_ID_PREFIXES","ELEMENT_HIDDEN_CLASS","COMPONENT_NODE_ID_PREFIX","applyElementVisibility","command","selector","root","document","elements","Array","from","querySelectorAll","element","hidden","classList","contains","nextHidden","toggle","HTMLElement","style","display","removeProperty","length","pendingVisibilityTimers","Map","activeDismissers","resetElementVisibilityChoreography","timer","values","clearTimeout","clear","teardown","disarmDismissal","get","delete","armDismissal","dismissOn","doc","dismiss","onKeyDown","event","key","onPointerDown","target","closest","includes","addEventListener","set","removeEventListener","runElementVisibilityStep","options","pending","apply","shown","querySelector","delay","Number","delayMs","isFinite","setTimeout","LEAF_ID_NAMESPACE_PREFIXES","isComponentGraft","id","startsWith","normalizeLeafId","value","String","prefix","slice","leafIdsMatch","a","b","left","right","endsWith","leafIdFromSelector","match","exec","undefined","expandLeafSelector","rawId","base","exact","map","join","dispatchUiCommand","eventName","detail","window","dispatchEvent","CustomEvent","subscribeUiCommands","handler","listener","DRAWER_COMMAND_EVENT","dispatchDrawerCommand","nodeId","subscribeDrawerCommands","MENU_COMMAND_EVENT","dispatchMenuCommand","hover","subscribeMenuCommands","resolveStepTarget","matches","find","getClientRects","scrollContainerOf","view","ownerDocument","node","parentElement","body","documentElement","overflowY","getComputedStyle","scrollHeight","clientHeight","focusWithoutScrolling","tabIndex","hasAttribute","setAttribute","removeAttribute","once","focus","preventScroll","runScrollToStep","defaultView","reduceMotion","matchMedia","behavior","scrollIntoView","block","requested","offsetPx","offset","Math","min","max","top","scrollY","getBoundingClientRect","scrollTo","VIDEO_COMMAND_EVENT","dispatchVideoCommand","cancelable","subscribeVideoCommands","preventDefault","runPlayVideoStep","VISIBILITY_BANDS","VISIBILITY_BAND_MEDIA","mobile","tablet","desktop"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-ui.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Element UI choreography contract (AGL-562): the shared pieces the\n * interactions executor (marketing site runtime), the drawer element\n * (plugins-mui), and the besigner authoring surfaces agree on —\n * show/hide semantics, the drawer command event bus, and the responsive\n * visibility media bands. Pure constants + DOM helpers; every function\n * that touches `window`/`document` is only ever called client-side.\n */\n\nimport { SCROLL_TO_MAX_OFFSET_PX, type ScrollToBehavior } from './site-interactions'\nimport { LAYOUT_NODE_ID_PREFIXES } from './compose-layout-nodes'\nimport { ELEMENT_HIDDEN_CLASS } from './element-hidden-style'\nimport { COMPONENT_NODE_ID_PREFIX } from './reusable-component-keys'\n\nexport * from './element-hidden-style'\n\nexport type ElementVisibilityCommand = 'show' | 'hide' | 'toggle'\n\n/**\n * The event an element hears when a show/hide/toggle step reaches it\n * (AGL-3717), dispatched ON the element after its hidden class has changed.\n *\n * Most elements need nothing more than the class. One whose \"shown\" is more\n * than a CSS rule — a lightbox that opens a dialog, a panel that loads its\n * contents only when revealed — subscribes here and does the rest itself, so\n * the interactions system's existing \"Show an element\" step opens it from any\n * button, link or picture without a step type of its own. Generic on purpose:\n * any element, from any plugin, may answer.\n */\nexport const ELEMENT_VISIBILITY_EVENT = 'aglyn:element-visibility'\n\nexport interface ElementVisibilityDetail {\n /** Whether the element is hidden now that the step has run. */\n hidden: boolean\n}\n\n/** Hears visibility steps that reach `element`; returns the unsubscriber. */\nexport function subscribeElementVisibility(\n element: EventTarget,\n handler: (detail: ElementVisibilityDetail) => void,\n): () => void {\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<ElementVisibilityDetail>).detail\n if (detail && typeof detail.hidden === 'boolean') handler(detail)\n }\n element.addEventListener(ELEMENT_VISIBILITY_EVENT, listener)\n return () => element.removeEventListener(ELEMENT_VISIBILITY_EVENT, listener)\n}\n\n/**\n * Applies a show/hide/toggle command to every element the selector\n * matches. Hide adds the shared hidden class; show removes it AND any\n * inline `display: none` so an element hidden either way reveals; toggle\n * flips per element. Each element then hears\n * {@link ELEMENT_VISIBILITY_EVENT}. Returns the number of elements touched\n * (0 for a selector that matches nothing — steps never throw).\n */\nexport function applyElementVisibility(\n command: ElementVisibilityCommand,\n selector: string,\n root: ParentNode = document,\n): number {\n let elements: Element[]\n try {\n elements = Array.from(root.querySelectorAll(selector))\n } catch {\n return 0 // Invalid stored selector — never break the page.\n }\n for (const element of elements) {\n const hidden = element.classList.contains(ELEMENT_HIDDEN_CLASS)\n const nextHidden = command === 'toggle' ? !hidden : command === 'hide'\n element.classList.toggle(ELEMENT_HIDDEN_CLASS, nextHidden)\n if (!nextHidden && element instanceof HTMLElement) {\n // Clear a leftover inline hide (e.g. from custom CSS experiments)\n // so \"show\" always actually reveals.\n if (element.style.display === 'none') element.style.removeProperty('display')\n }\n if (typeof CustomEvent === 'function') {\n element.dispatchEvent(\n new CustomEvent<ElementVisibilityDetail>(ELEMENT_VISIBILITY_EVENT, {\n detail: { hidden: nextHidden },\n }),\n )\n }\n }\n return elements.length\n}\n\n/* ── Visibility choreography: grace delays + self-dismissal (AGL-589) ── */\n\nexport interface ElementVisibilityOptions {\n /** Defer the change; a later visibility step on the same selector\n * cancels the pending one — the hover grace period. */\n delayMs?: number\n /** Self-dismiss a SHOWN element on Escape / a pointerdown outside it. */\n dismissOn?: Array<'escape' | 'outsideClick'>\n}\n\n// Selector-keyed registries. One document per site runtime, so module\n// scope is the right lifetime; reset exists for tests.\nconst pendingVisibilityTimers = new Map<string, ReturnType<typeof setTimeout>>()\nconst activeDismissers = new Map<string, () => void>()\n\n/** Tears down all pending timers and dismiss listeners (tests). */\nexport function resetElementVisibilityChoreography(): void {\n for (const timer of pendingVisibilityTimers.values()) clearTimeout(timer)\n pendingVisibilityTimers.clear()\n for (const teardown of activeDismissers.values()) teardown()\n activeDismissers.clear()\n}\n\nfunction disarmDismissal(selector: string): void {\n activeDismissers.get(selector)?.()\n activeDismissers.delete(selector)\n}\n\nfunction armDismissal(\n selector: string,\n dismissOn: NonNullable<ElementVisibilityOptions['dismissOn']>,\n doc: Document,\n): void {\n disarmDismissal(selector)\n if (!dismissOn.length) return\n const dismiss = () => {\n applyElementVisibility('hide', selector, doc)\n disarmDismissal(selector)\n }\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') dismiss()\n }\n const onPointerDown = (event: Event) => {\n const target = event.target as Element | null\n // Inside the shown element (or its trigger sharing a wrapper that\n // matches the selector) never dismisses — only genuine outside taps.\n if (target?.closest?.(selector)) return\n dismiss()\n }\n if (dismissOn.includes('escape')) {\n doc.addEventListener('keydown', onKeyDown)\n }\n if (dismissOn.includes('outsideClick')) {\n doc.addEventListener('pointerdown', onPointerDown)\n }\n activeDismissers.set(selector, () => {\n doc.removeEventListener('keydown', onKeyDown)\n doc.removeEventListener('pointerdown', onPointerDown)\n })\n}\n\n/**\n * Runs an element-visibility step with menu-grade choreography (AGL-589):\n *\n * - `delayMs` defers the change; ANY later visibility step on the same\n * selector cancels the pending one first. Hover menus author\n * hover-enter → show (immediate, cancels a pending hide) and\n * hover-leave → hide with a small delay — the classic grace period.\n * - `dismissOn` arms Escape / outside-pointerdown listeners while the\n * element is shown (armed AFTER the change applies, so the click that\n * opened it can never instantly close it) and disarms them on hide.\n *\n * Without options this is exactly {@link applyElementVisibility}.\n */\nexport function runElementVisibilityStep(\n command: ElementVisibilityCommand,\n selector: string,\n options: ElementVisibilityOptions = {},\n doc: Document = document,\n): void {\n // Later intent wins: cancel whatever is pending for this selector.\n const pending = pendingVisibilityTimers.get(selector)\n if (pending != null) {\n clearTimeout(pending)\n pendingVisibilityTimers.delete(selector)\n }\n const apply = () => {\n pendingVisibilityTimers.delete(selector)\n applyElementVisibility(command, selector, doc)\n const dismissOn = options.dismissOn ?? []\n if (!dismissOn.length) return\n // Toggle can land either way — read the DOM for the outcome.\n let shown = command === 'show'\n if (command === 'toggle') {\n try {\n const element = doc.querySelector(selector)\n shown =\n element != null &&\n !element.classList.contains(ELEMENT_HIDDEN_CLASS)\n } catch {\n shown = false\n }\n }\n if (command === 'hide' || !shown) disarmDismissal(selector)\n else armDismissal(selector, dismissOn, doc)\n }\n const delay = Number(options.delayMs)\n if (Number.isFinite(delay) && delay > 0) {\n pendingVisibilityTimers.set(selector, setTimeout(apply, delay))\n } else {\n apply()\n }\n}\n\n/* ── Layout-namespace-insensitive id matching (AGL-573) ─────────────── */\n\n/**\n * Composition namespace prefixes the live runtime can graft onto a node's\n * canvas id. Layout nodes are namespaced during screen composition\n * ({@link LAYOUT_NODE_ID_PREFIXES}) so they can never collide with screen\n * ids — which means an interaction authored against the RAW canvas id\n * (what the besigner builder emits: `menuNodeId` and the\n * `[data-aglyn=\"leaf:<id>\"]` selector) is not byte-equal to the id the\n * live, layout-composed DOM carries.\n *\n * One entry per level of the layout chain (AGL-703): a layout nested\n * inside another carries `layout2__`, and an interaction authored on it\n * must still match. Being a list was already the design; nesting is the\n * case it was left open for.\n */\nconst LEAF_ID_NAMESPACE_PREFIXES = LAYOUT_NODE_ID_PREFIXES\n\n/**\n * Whether an id looks like a reusable-component graft (AGL-1229).\n *\n * A graft's namespace is `cmp__{instanceId}__`, and unlike the layout\n * prefixes it CANNOT be enumerated — there is one per placement, and the\n * instance id is itself often composed: the nav really ships\n * `cmp__layout__52Ef-3t6yd___R91yATrXH`.\n *\n * It also cannot be reliably PARSED off. With a raw id that starts with `_`\n * (nanoid does that routinely) the `___` boundary splits two ways and both\n * are structurally valid, so stripping guesses wrong half the time. Hence\n * {@link leafIdsMatch} compares by suffix instead of normalizing — a\n * composed id always ends in the raw id it was built from.\n */\nconst isComponentGraft = (id: string) =>\n id.startsWith(COMPONENT_NODE_ID_PREFIX)\n\n/**\n * Strips a single leading LAYOUT namespace prefix from a node/leaf id so ids\n * authored against the raw canvas id and ids stamped on the live\n * layout-composed DOM compare equal (AGL-573). Idempotent for an\n * already-raw id, and only a *leading* prefix is removed, so unrelated ids\n * can never be coerced into colliding.\n *\n * Deliberately does NOT try to strip a reusable-component graft — that\n * namespace is unparseable (see {@link isComponentGraft}); `leafIdsMatch`\n * and `expandLeafSelector` handle grafts by suffix.\n *\n * @example\n * normalizeLeafId('layout___5I3TBXywa') // → '_5I3TBXywa'\n * normalizeLeafId('_5I3TBXywa') // → '_5I3TBXywa'\n */\nexport function normalizeLeafId(id: string | undefined | null): string {\n const value = String(id ?? '')\n for (const prefix of LEAF_ID_NAMESPACE_PREFIXES) {\n if (value.startsWith(prefix)) return value.slice(prefix.length)\n }\n return value\n}\n\n/**\n * Whether two node ids address the same node ignoring composition\n * namespace prefixes (AGL-573, extended to reusable-component grafts by\n * AGL-1229) — the durable half of the fix for interactions authored on\n * composed elements, whose stored command id is the raw canvas id while the\n * live element's id is namespaced.\n *\n * Layout prefixes are stripped exactly. A component graft is matched by\n * SUFFIX instead, because its namespace cannot be parsed off unambiguously\n * (see {@link isComponentGraft}) — a composed id always ends in `__` plus\n * the raw id it was built from, and a raw id is a single nanoid with no\n * `__` boundary to spare, so the suffix cannot reach an unrelated node.\n *\n * Two empty ids never match (a missing id is not a wildcard).\n */\nexport function leafIdsMatch(\n a: string | undefined | null,\n b: string | undefined | null,\n): boolean {\n const left = normalizeLeafId(a)\n const right = normalizeLeafId(b)\n if (left === '' || right === '') return false\n if (left === right) return true\n if (isComponentGraft(right) && right.endsWith(`__${left}`)) return true\n if (isComponentGraft(left) && left.endsWith(`__${right}`)) return true\n return false\n}\n\n/**\n * Extracts the leaf id from a canonical `[data-aglyn=\"leaf:<id>\"]`\n * selector (the exact form the renderer and the interaction builder emit),\n * or `undefined` for any other selector.\n */\nexport function leafIdFromSelector(selector: string): string | undefined {\n const match = /^\\s*\\[data-aglyn=\"leaf:(.+)\"\\]\\s*$/.exec(selector)\n return match?.[1] || undefined\n}\n\n/**\n * Rewrites a `[data-aglyn=\"leaf:<id>\"]` selector so it ALSO matches the\n * same node once composition has namespaced its live id (AGL-573, extended\n * to reusable components by AGL-1229):\n *\n * [data-aglyn=\"leaf:_5I3TBXywa\"]\n * → [data-aglyn=\"leaf:_5I3TBXywa\"],\n * [data-aglyn=\"leaf:layout___5I3TBXywa\"],\n * [data-aglyn$=\"___5I3TBXywa\"]\n *\n * The layout alternatives stay enumerated and exactly anchored. The last is\n * a SUFFIX match, and it is the only way to reach a reusable-component\n * graft: its namespace is `cmp__{instanceId}__`, one per placement, so\n * there is no finite prefix list to enumerate.\n *\n * The suffix cannot collide. A composed id always ends in the raw id it was\n * built from, so matching `__` + the exact raw id can only reach a\n * composition OF THAT id — reaching an unrelated node would need that\n * node's own raw id to end in `__` plus a full id, and raw ids are a single\n * fixed-length nanoid with no `__` boundary to spare.\n *\n * Any non-leaf selector — a plain CSS selector an author typed by hand —\n * passes through verbatim.\n */\nexport function expandLeafSelector(selector: string): string {\n const rawId = leafIdFromSelector(selector)\n if (!rawId) return selector\n const base = normalizeLeafId(rawId)\n const exact = [\n base,\n ...LEAF_ID_NAMESPACE_PREFIXES.map((prefix) => `${prefix}${base}`),\n ].map((id) => `[data-aglyn=\"leaf:${id}\"]`)\n return [...exact, `[data-aglyn$=\"__${base}\"]`].join(', ')\n}\n\n/* ── UI command buses (drawer AGL-562, menu AGL-568) ────────────────── */\n\n/**\n * Shared open/close/toggle transport for popup-like elements. Keeping\n * the transport at the DOM level lets the marketing automations engine\n * drive drawers and menus without importing the mui plugin (and vice\n * versa). Each surface keeps its own event name so the drawer contract\n * shipped in AGL-562 stays byte-for-byte stable.\n */\ninterface UiCommandDetail {\n command: 'open' | 'close' | 'toggle'\n nodeId?: string\n}\n\nfunction dispatchUiCommand<TDetail extends UiCommandDetail>(\n eventName: string,\n detail: TDetail,\n): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<TDetail>(eventName, { detail }))\n}\n\nfunction subscribeUiCommands<TDetail extends UiCommandDetail>(\n eventName: string,\n handler: (detail: TDetail) => void,\n): () => void {\n if (typeof window === 'undefined') return () => undefined\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<TDetail>).detail\n if (detail?.command) handler(detail)\n }\n window.addEventListener(eventName, listener)\n return () => window.removeEventListener(eventName, listener)\n}\n\nexport type DrawerCommand = 'open' | 'close' | 'toggle'\n\n/** Window event carrying a drawer command (AGL-562). */\nexport const DRAWER_COMMAND_EVENT = 'aglyn:drawer-command'\n\nexport interface DrawerCommandDetail {\n command: DrawerCommand\n /** Target drawer's canvas node id; absent = the page's first drawer. */\n nodeId?: string\n}\n\n/** Dispatches a drawer command onto the window event bus. */\nexport function dispatchDrawerCommand(\n command: DrawerCommand,\n nodeId?: string,\n): void {\n dispatchUiCommand<DrawerCommandDetail>(DRAWER_COMMAND_EVENT, {\n command,\n ...(nodeId ? { nodeId } : {}),\n })\n}\n\n/**\n * Subscribes to drawer commands; returns the unsubscriber. The handler\n * receives every command — drawer instances filter by their own node id\n * (or first-registered status for broadcasts).\n */\nexport function subscribeDrawerCommands(\n handler: (detail: DrawerCommandDetail) => void,\n): () => void {\n return subscribeUiCommands(DRAWER_COMMAND_EVENT, handler)\n}\n\nexport type MenuCommand = 'open' | 'close' | 'toggle'\n\n/**\n * Window event carrying a nav-menu command (AGL-568): the interactions\n * system's open/close/toggle steps address Dropdown Menu and Mega Menu\n * elements over this bus, exactly like drawers — no bespoke \"open on\"\n * attribute involved.\n */\nexport const MENU_COMMAND_EVENT = 'aglyn:menu-command'\n\nexport interface MenuCommandDetail {\n command: MenuCommand\n /** Target menu's canvas node id; absent = the page's first menu. */\n nodeId?: string\n /**\n * Set when the command came from a hover trigger: a menu opened this\n * way closes itself once the pointer leaves the trigger + panel\n * surface (standard hover-menu UX), while click/command opens stay\n * put until an explicit close, click-away, or Escape.\n */\n hover?: boolean\n}\n\n/** Dispatches a menu command onto the window event bus. */\nexport function dispatchMenuCommand(\n command: MenuCommand,\n nodeId?: string,\n options?: { hover?: boolean },\n): void {\n dispatchUiCommand<MenuCommandDetail>(MENU_COMMAND_EVENT, {\n command,\n ...(nodeId ? { nodeId } : {}),\n ...(options?.hover ? { hover: true } : {}),\n })\n}\n\n/**\n * Subscribes to menu commands; returns the unsubscriber. The handler\n * receives every command — menu instances filter by their own node id\n * (or first-registered status for broadcasts).\n */\nexport function subscribeMenuCommands(\n handler: (detail: MenuCommandDetail) => void,\n): () => void {\n return subscribeUiCommands(MENU_COMMAND_EVENT, handler)\n}\n\n/* ── One-target steps: scroll to an element, play a video (AGL-2867) ── */\n\n/**\n * The element a one-target step acts on: the first match that has a layout\n * box, or null.\n *\n * Every match is read rather than only the first, because one element can\n * legitimately appear twice — an expanded leaf selector reaches each\n * composition of an id, and a page may carry a desktop and a mobile copy with\n * one hidden at each breakpoint. The copy the visitor can see is the one they\n * meant.\n *\n * A match with no box is never acted on. Scrolling to a `display: none`\n * element lands at the top of the page, and pressing a hidden player starts a\n * film nobody can see, so a target that is not on the page in the layout\n * sense makes the step a no-op, as a missing or deleted one does.\n */\nexport function resolveStepTarget(\n selector: string,\n root: ParentNode = document,\n): HTMLElement | SVGElement | null {\n let matches: Element[]\n try {\n matches = Array.from(root.querySelectorAll(selector))\n } catch {\n return null // Invalid stored selector — never break the page.\n }\n const shown = matches.find((element) => element.getClientRects().length > 0)\n return (shown as HTMLElement | SVGElement | undefined) ?? null\n}\n\nexport interface ScrollToStepOptions {\n /** Absent means `smooth`; reduced motion overrides either to `instant`. */\n behavior?: ScrollToBehavior\n /** Pixels left above the target; clamped to 0–{@link SCROLL_TO_MAX_OFFSET_PX}. */\n offsetPx?: number\n}\n\n/** The nearest ancestor that scrolls its own content, below the document. */\nfunction scrollContainerOf(element: Element, view: Window): Element | null {\n const doc = element.ownerDocument\n for (\n let node = element.parentElement;\n node && node !== doc.body && node !== doc.documentElement;\n node = node.parentElement\n ) {\n const { overflowY } = view.getComputedStyle(node)\n if (\n (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&\n node.scrollHeight > node.clientHeight\n ) {\n return node\n }\n }\n return null\n}\n\n/**\n * Focus for a keyboard visitor, without the second scroll `focus()` would\n * start on its own.\n *\n * An element that cannot take focus is given `tabindex=\"-1\"` for the length\n * of the visit to it — focusable by script, never added to the Tab order —\n * and loses it again on blur, so the author's markup is left as it was.\n */\nfunction focusWithoutScrolling(element: HTMLElement | SVGElement): void {\n if (element.tabIndex < 0 && !element.hasAttribute('tabindex')) {\n element.setAttribute('tabindex', '-1')\n element.addEventListener('blur', () => element.removeAttribute('tabindex'), {\n once: true,\n })\n }\n element.focus({ preventScroll: true })\n}\n\n/**\n * Runs a `scrollTo` step: brings the target to the top of the window, less\n * the offset, and moves focus to it. Returns whether there was a target.\n *\n * The position is computed and handed to the window rather than asked of\n * `scrollIntoView`, because the offset has nowhere to go in that call except\n * a `scroll-margin` written onto the author's element. A target inside a box\n * that scrolls its own content is the exception, and there the box has to be\n * scrolled too, which `scrollIntoView` does for every such ancestor at once;\n * the offset is for a header over the page, not for a panel's own edge.\n *\n * Reduced motion is read at the moment of the press, so a visitor who changes\n * the setting mid-visit gets what they asked for on the next one.\n */\nexport function runScrollToStep(\n selector: string,\n options: ScrollToStepOptions = {},\n doc: Document = document,\n): boolean {\n const view = doc.defaultView\n const target = resolveStepTarget(selector, doc)\n if (!view || !target) return false\n const reduceMotion =\n view.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n const behavior: ScrollBehavior =\n reduceMotion || options.behavior === 'instant' ? 'instant' : 'smooth'\n if (scrollContainerOf(target, view)) {\n target.scrollIntoView({ behavior, block: 'start' })\n } else {\n const requested = Number(options.offsetPx)\n const offset = Number.isFinite(requested)\n ? Math.min(Math.max(requested, 0), SCROLL_TO_MAX_OFFSET_PX)\n : 0\n const top = view.scrollY + target.getBoundingClientRect().top - offset\n view.scrollTo({ top: Math.max(0, top), behavior })\n }\n focusWithoutScrolling(target)\n return true\n}\n\n/**\n * The event a Video element answers with its own poster press.\n *\n * Dispatched ON the element, where the drawer and menu commands go to\n * `window`. Those buses need an address because a command with none means\n * \"the page's first drawer\"; a `playVideo` step has already found its element\n * through its selector, so the element that receives the event is the one that\n * should answer, with no id to parse or compare.\n */\nexport const VIDEO_COMMAND_EVENT = 'aglyn:video-command'\n\nexport type VideoCommand = 'play'\n\nexport interface VideoCommandDetail {\n command: VideoCommand\n}\n\n/**\n * Sends a command to an element. Returns whether a Video element answered —\n * a listener registered through {@link subscribeVideoCommands} cancels the\n * event to say so — which is false for an element that is not a Video.\n */\nexport function dispatchVideoCommand(\n element: EventTarget,\n command: VideoCommand,\n): boolean {\n return !element.dispatchEvent(\n new CustomEvent<VideoCommandDetail>(VIDEO_COMMAND_EVENT, {\n detail: { command },\n cancelable: true,\n }),\n )\n}\n\n/** Answers video commands sent to `element`; returns the unsubscriber. */\nexport function subscribeVideoCommands(\n element: EventTarget,\n handler: (detail: VideoCommandDetail) => void,\n): () => void {\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<VideoCommandDetail>).detail\n if (!detail?.command) return\n event.preventDefault()\n handler(detail)\n }\n element.addEventListener(VIDEO_COMMAND_EVENT, listener)\n return () => element.removeEventListener(VIDEO_COMMAND_EVENT, listener)\n}\n\n/**\n * Runs a `playVideo` step: presses the poster of the Video element the\n * selector names. Returns whether a Video answered.\n */\nexport function runPlayVideoStep(\n selector: string,\n doc: Document = document,\n): boolean {\n const target = resolveStepTarget(selector, doc)\n return target ? dispatchVideoCommand(target, 'play') : false\n}\n\n/* ── Responsive visibility bands (AGL-562) ──────────────────────────── */\n\n/**\n * The three authoring-facing visibility bands and the sx media-query\n * keys the styles panel writes `display: none` under. Range-scoped\n * queries (not MUI's mobile-first responsive objects) so hiding one band\n * never needs a \"restore\" display value on the others — the element's\n * natural display simply keeps applying outside the hidden range.\n * Boundaries follow the MUI defaults the device preview uses: mobile\n * under 600, tablet 600–899, desktop 900 and up.\n */\nexport const VISIBILITY_BANDS = ['mobile', 'tablet', 'desktop'] as const\n\nexport type VisibilityBand = (typeof VISIBILITY_BANDS)[number]\n\nexport const VISIBILITY_BAND_MEDIA: Record<VisibilityBand, string> = {\n mobile: '@media (max-width:599.95px)',\n tablet: '@media (min-width:600px) and (max-width:899.95px)',\n desktop: '@media (min-width:900px)',\n}\n"],"names":["SCROLL_TO_MAX_OFFSET_PX","LAYOUT_NODE_ID_PREFIXES","ELEMENT_HIDDEN_CLASS","COMPONENT_NODE_ID_PREFIX","ELEMENT_VISIBILITY_EVENT","subscribeElementVisibility","element","handler","listener","event","detail","hidden","addEventListener","removeEventListener","applyElementVisibility","command","selector","root","document","elements","Array","from","querySelectorAll","classList","contains","nextHidden","toggle","HTMLElement","style","display","removeProperty","CustomEvent","dispatchEvent","length","pendingVisibilityTimers","Map","activeDismissers","resetElementVisibilityChoreography","timer","values","clearTimeout","clear","teardown","disarmDismissal","get","delete","armDismissal","dismissOn","doc","dismiss","onKeyDown","key","onPointerDown","target","closest","includes","set","runElementVisibilityStep","options","pending","apply","shown","querySelector","delay","Number","delayMs","isFinite","setTimeout","LEAF_ID_NAMESPACE_PREFIXES","isComponentGraft","id","startsWith","normalizeLeafId","value","String","prefix","slice","leafIdsMatch","a","b","left","right","endsWith","leafIdFromSelector","match","exec","undefined","expandLeafSelector","rawId","base","exact","map","join","dispatchUiCommand","eventName","window","subscribeUiCommands","DRAWER_COMMAND_EVENT","dispatchDrawerCommand","nodeId","subscribeDrawerCommands","MENU_COMMAND_EVENT","dispatchMenuCommand","hover","subscribeMenuCommands","resolveStepTarget","matches","find","getClientRects","scrollContainerOf","view","ownerDocument","node","parentElement","body","documentElement","overflowY","getComputedStyle","scrollHeight","clientHeight","focusWithoutScrolling","tabIndex","hasAttribute","setAttribute","removeAttribute","once","focus","preventScroll","runScrollToStep","defaultView","reduceMotion","matchMedia","behavior","scrollIntoView","block","requested","offsetPx","offset","Math","min","max","top","scrollY","getBoundingClientRect","scrollTo","VIDEO_COMMAND_EVENT","dispatchVideoCommand","cancelable","subscribeVideoCommands","preventDefault","runPlayVideoStep","VISIBILITY_BANDS","VISIBILITY_BAND_MEDIA","mobile","tablet","desktop"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installForeignDomGuard(): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Keeps a browser translator from crashing React (facebook/react#11538).
|
|
18
|
+
*
|
|
19
|
+
* Chrome's built-in Translate (and extensions like it) replaces a text node
|
|
20
|
+
* React rendered with a `<font>` wrapper holding the translation. React still
|
|
21
|
+
* holds the ORIGINAL node, so the next commit that removes it, or inserts
|
|
22
|
+
* before it, asks a parent that no longer has it as a child and the DOM
|
|
23
|
+
* throws `NotFoundError: Failed to execute 'removeChild' on 'Node'`. Nothing
|
|
24
|
+
* catches a commit-phase throw short of the root boundary, so the visitor
|
|
25
|
+
* gets the "Something went wrong" screen.
|
|
26
|
+
*
|
|
27
|
+
* That happened on 2026-10-09 22:30Z: a visitor from Istanbul signed up, the
|
|
28
|
+
* console's `/` landing crashed 41 seconds later (preceded by a #418 text
|
|
29
|
+
* mismatch on aglyn.com, the translator's other signature), `global-error`
|
|
30
|
+
* replaced `<body>`, and App Check's reCAPTCHA then failed twice because the
|
|
31
|
+
* container it had appended to `<body>` was gone. The same pair hit
|
|
32
|
+
* aglyn.com on 2026-10-04.
|
|
33
|
+
*
|
|
34
|
+
* The guard answers exactly the mis-parented case and nothing else: removing
|
|
35
|
+
* a node from a parent it is not in is a no-op (the node is already out of
|
|
36
|
+
* the place React believed it was), and inserting before a reference that is
|
|
37
|
+
* not a child appends instead of throwing. Every well-formed call goes
|
|
38
|
+
* straight to the native method. React's own recommendation for translated
|
|
39
|
+
* pages is this patch or `translate="no"`, and the second would take
|
|
40
|
+
* translation away from the very visitors who need it.
|
|
41
|
+
*
|
|
42
|
+
* Framework-free and tiny on purpose: it runs on every published tenant page
|
|
43
|
+
* as well as the console, at module scope beside the error beacon so it is in
|
|
44
|
+
* place before hydration commits anything. No-op during SSR and on repeat
|
|
45
|
+
* installs.
|
|
46
|
+
*/ const GUARDED = Symbol.for('aglyn.foreignDomGuard');
|
|
47
|
+
export function installForeignDomGuard() {
|
|
48
|
+
if (typeof Node !== 'function' || !Node.prototype) return;
|
|
49
|
+
const proto = Node.prototype;
|
|
50
|
+
if (proto[GUARDED]) return;
|
|
51
|
+
proto[GUARDED] = true;
|
|
52
|
+
const removeChild = proto.removeChild;
|
|
53
|
+
proto.removeChild = function(child) {
|
|
54
|
+
if (child && child.parentNode !== this) return child;
|
|
55
|
+
return removeChild.call(this, child);
|
|
56
|
+
};
|
|
57
|
+
const insertBefore = proto.insertBefore;
|
|
58
|
+
proto.insertBefore = function(node, reference) {
|
|
59
|
+
if (reference && reference.parentNode !== this) {
|
|
60
|
+
return insertBefore.call(this, node, null);
|
|
61
|
+
}
|
|
62
|
+
return insertBefore.call(this, node, reference);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=foreign-dom-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/foreign-dom-guard.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Keeps a browser translator from crashing React (facebook/react#11538).\n *\n * Chrome's built-in Translate (and extensions like it) replaces a text node\n * React rendered with a `<font>` wrapper holding the translation. React still\n * holds the ORIGINAL node, so the next commit that removes it, or inserts\n * before it, asks a parent that no longer has it as a child and the DOM\n * throws `NotFoundError: Failed to execute 'removeChild' on 'Node'`. Nothing\n * catches a commit-phase throw short of the root boundary, so the visitor\n * gets the \"Something went wrong\" screen.\n *\n * That happened on 2026-10-09 22:30Z: a visitor from Istanbul signed up, the\n * console's `/` landing crashed 41 seconds later (preceded by a #418 text\n * mismatch on aglyn.com, the translator's other signature), `global-error`\n * replaced `<body>`, and App Check's reCAPTCHA then failed twice because the\n * container it had appended to `<body>` was gone. The same pair hit\n * aglyn.com on 2026-10-04.\n *\n * The guard answers exactly the mis-parented case and nothing else: removing\n * a node from a parent it is not in is a no-op (the node is already out of\n * the place React believed it was), and inserting before a reference that is\n * not a child appends instead of throwing. Every well-formed call goes\n * straight to the native method. React's own recommendation for translated\n * pages is this patch or `translate=\"no\"`, and the second would take\n * translation away from the very visitors who need it.\n *\n * Framework-free and tiny on purpose: it runs on every published tenant page\n * as well as the console, at module scope beside the error beacon so it is in\n * place before hydration commits anything. No-op during SSR and on repeat\n * installs.\n */\n\nconst GUARDED = Symbol.for('aglyn.foreignDomGuard')\n\nexport function installForeignDomGuard(): void {\n if (typeof Node !== 'function' || !Node.prototype) return\n const proto = Node.prototype as Node & { [GUARDED]?: true }\n if (proto[GUARDED]) return\n proto[GUARDED] = true\n\n const removeChild = proto.removeChild\n proto.removeChild = function <T extends Node>(this: Node, child: T): T {\n if (child && child.parentNode !== this) return child\n return removeChild.call(this, child) as T\n }\n\n const insertBefore = proto.insertBefore\n proto.insertBefore = function <T extends Node>(\n this: Node,\n node: T,\n reference: Node | null,\n ): T {\n if (reference && reference.parentNode !== this) {\n return insertBefore.call(this, node, null) as T\n }\n return insertBefore.call(this, node, reference) as T\n }\n}\n"],"names":["GUARDED","Symbol","for","installForeignDomGuard","Node","prototype","proto","removeChild","child","parentNode","call","insertBefore","node","reference"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GAED,MAAMA,UAAUC,OAAOC,GAAG,CAAC;AAE3B,OAAO,SAASC;IACd,IAAI,OAAOC,SAAS,cAAc,CAACA,KAAKC,SAAS,EAAE;IACnD,MAAMC,QAAQF,KAAKC,SAAS;IAC5B,IAAIC,KAAK,CAACN,QAAQ,EAAE;IACpBM,KAAK,CAACN,QAAQ,GAAG;IAEjB,MAAMO,cAAcD,MAAMC,WAAW;IACrCD,MAAMC,WAAW,GAAG,SAAsCC,KAAQ;QAChE,IAAIA,SAASA,MAAMC,UAAU,KAAK,IAAI,EAAE,OAAOD;QAC/C,OAAOD,YAAYG,IAAI,CAAC,IAAI,EAAEF;IAChC;IAEA,MAAMG,eAAeL,MAAMK,YAAY;IACvCL,MAAMK,YAAY,GAAG,SAEnBC,IAAO,EACPC,SAAsB;QAEtB,IAAIA,aAAaA,UAAUJ,UAAU,KAAK,IAAI,EAAE;YAC9C,OAAOE,aAAaD,IAAI,CAAC,IAAI,EAAEE,MAAM;QACvC;QACA,OAAOD,aAAaD,IAAI,CAAC,IAAI,EAAEE,MAAMC;IACvC;AACF"}
|
|
@@ -163,6 +163,17 @@ export interface FormDocument<N = AglynNodeSchema> {
|
|
|
163
163
|
fields: FormFieldDecl[];
|
|
164
164
|
/** Names the entry in `fields` that IS the marketing opt-in. */
|
|
165
165
|
consentFieldName?: string;
|
|
166
|
+
/**
|
|
167
|
+
* A SIGN-UP form: submitting it is the opt-in.
|
|
168
|
+
*
|
|
169
|
+
* For a form whose one purpose is subscribing ("Get product updates",
|
|
170
|
+
* an email field and a Subscribe button), where a box to tick beside the
|
|
171
|
+
* button would ask the same question twice. Set by the merchant on the
|
|
172
|
+
* form, never inferred, so every other form keeps the rule that the fact
|
|
173
|
+
* of submission is not consent. Read by `/api/forms/submit`, which then
|
|
174
|
+
* records the opt-in and makes the person at least a subscriber.
|
|
175
|
+
*/
|
|
176
|
+
optInOnSubmit?: boolean;
|
|
166
177
|
routing?: FormRouting;
|
|
167
178
|
legacyMatch?: FormLegacyMatch;
|
|
168
179
|
stats?: FormStats;
|