assign-gingerly 0.0.52 → 0.0.54

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/resolveValues.js CHANGED
@@ -98,6 +98,40 @@ function navigatePath(source, parts, withMethods) {
98
98
  }
99
99
  return current;
100
100
  }
101
+ /**
102
+ * Resolve path strings and protocol references within an array.
103
+ * Recurses into nested arrays and plain objects. Non-string elements,
104
+ * class instances, and other non-plain objects pass through unchanged.
105
+ */
106
+ async function resolveArray(arr, source, aliasMap, withMethods, protocols, options) {
107
+ const result = [];
108
+ for (const item of arr) {
109
+ if (typeof item === 'string' && item.startsWith('?.')) {
110
+ const aliased = applyAliases(item, aliasMap);
111
+ const parts = parseCachedPath(aliased);
112
+ result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
113
+ }
114
+ else if (typeof item === 'string' && protocols && hasProtocol(item)) {
115
+ result.push(await resolveProtocolValue(item, protocols, options));
116
+ }
117
+ else if (Array.isArray(item)) {
118
+ result.push(await resolveArray(item, source, aliasMap, withMethods, protocols, options));
119
+ }
120
+ else if (item && typeof item === 'object') {
121
+ const proto = Object.getPrototypeOf(item);
122
+ if (proto === Object.prototype || proto === null) {
123
+ result.push(await resolveValues(item, source, options));
124
+ }
125
+ else {
126
+ result.push(item);
127
+ }
128
+ }
129
+ else {
130
+ result.push(item);
131
+ }
132
+ }
133
+ return result;
134
+ }
101
135
  /**
102
136
  * Resolve RHS path strings in a pattern object against a source object.
103
137
  *
@@ -160,6 +194,21 @@ export async function resolveValues(pattern, source, options) {
160
194
  // Protocol-prefixed value — resolve asynchronously
161
195
  result[key] = await resolveProtocolValue(value, protocols, options);
162
196
  }
197
+ else if (Array.isArray(value)) {
198
+ // Resolve path strings and protocols within arrays (recursing into nested arrays)
199
+ result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
200
+ }
201
+ else if (typeof value === 'object' && value !== null) {
202
+ // Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
203
+ // Only recurse into plain objects — skip DOM elements, class instances, etc.
204
+ const proto = Object.getPrototypeOf(value);
205
+ if (proto === Object.prototype || proto === null) {
206
+ result[key] = await resolveValues(value, source, options);
207
+ }
208
+ else {
209
+ result[key] = value;
210
+ }
211
+ }
163
212
  else {
164
213
  result[key] = value;
165
214
  }
package/resolveValues.ts CHANGED
@@ -147,6 +147,43 @@ function navigatePath(
147
147
  return current;
148
148
  }
149
149
 
150
+ /**
151
+ * Resolve path strings and protocol references within an array.
152
+ * Recurses into nested arrays and plain objects. Non-string elements,
153
+ * class instances, and other non-plain objects pass through unchanged.
154
+ */
155
+ async function resolveArray(
156
+ arr: any[],
157
+ source: any,
158
+ aliasMap: Map<string, string>,
159
+ withMethods: Set<string> | undefined,
160
+ protocols: Record<string, (key: string) => any | Promise<any>> | undefined,
161
+ options?: ResolveValuesOptions
162
+ ): Promise<any[]> {
163
+ const result: any[] = [];
164
+ for (const item of arr) {
165
+ if (typeof item === 'string' && item.startsWith('?.')) {
166
+ const aliased = applyAliases(item, aliasMap);
167
+ const parts = parseCachedPath(aliased);
168
+ result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
169
+ } else if (typeof item === 'string' && protocols && hasProtocol(item)) {
170
+ result.push(await resolveProtocolValue(item, protocols, options));
171
+ } else if (Array.isArray(item)) {
172
+ result.push(await resolveArray(item, source, aliasMap, withMethods, protocols, options));
173
+ } else if (item && typeof item === 'object') {
174
+ const proto = Object.getPrototypeOf(item);
175
+ if (proto === Object.prototype || proto === null) {
176
+ result.push(await resolveValues(item, source, options));
177
+ } else {
178
+ result.push(item);
179
+ }
180
+ } else {
181
+ result.push(item);
182
+ }
183
+ }
184
+ return result;
185
+ }
186
+
150
187
  /**
151
188
  * Resolve RHS path strings in a pattern object against a source object.
152
189
  *
@@ -216,6 +253,18 @@ export async function resolveValues(
216
253
  } else if (typeof value === 'string' && protocols && hasProtocol(value)) {
217
254
  // Protocol-prefixed value — resolve asynchronously
218
255
  result[key] = await resolveProtocolValue(value, protocols, options);
256
+ } else if (Array.isArray(value)) {
257
+ // Resolve path strings and protocols within arrays (recursing into nested arrays)
258
+ result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
259
+ } else if (typeof value === 'object' && value !== null) {
260
+ // Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
261
+ // Only recurse into plain objects — skip DOM elements, class instances, etc.
262
+ const proto = Object.getPrototypeOf(value);
263
+ if (proto === Object.prototype || proto === null) {
264
+ result[key] = await resolveValues(value, source, options);
265
+ } else {
266
+ result[key] = value;
267
+ }
219
268
  } else {
220
269
  result[key] = value;
221
270
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * transitionHelper.js — Shared view transition coordination utility.
3
+ *
4
+ * Provides transition state management, the startViewTransition wrapper with
5
+ * cancel/re-entry protection, and one-time CSS style injection.
6
+ *
7
+ * Used by lazyLoad, lazyLoadSwitch, and available for external consumers
8
+ * like be-switched.
9
+ *
10
+ * @example
11
+ * import { withTransition, ensureHideStyle } from 'assign-gingerly/transitionHelper.js';
12
+ *
13
+ * ensureHideStyle(rootNode); // inject default .ag-hide style once
14
+ *
15
+ * withTransition(markerNode, 'show', true, () => {
16
+ * element.classList.remove('ag-hide');
17
+ * });
18
+ */
19
+
20
+ /**
21
+ * Module-level state map: marker node → transition state.
22
+ * WeakMap ensures cleanup when markers are GC'd.
23
+ */
24
+ const stateMap = new WeakMap();
25
+
26
+ /**
27
+ * Get or create transition state for a marker node.
28
+ */
29
+ export function getTransitionState(markerNode) {
30
+ let state = stateMap.get(markerNode);
31
+ if (!state) {
32
+ state = { showPending: false, hidePending: false };
33
+ stateMap.set(markerNode, state);
34
+ }
35
+ return state;
36
+ }
37
+
38
+ /**
39
+ * Execute a DOM mutation with optional view transition coordination.
40
+ *
41
+ * When `transitional` is true and `document.startViewTransition` is available:
42
+ * - Cancels any in-flight transition for this marker
43
+ * - Prevents re-entry (duplicate show/hide while one is pending)
44
+ * - Wraps the mutation in `document.startViewTransition`
45
+ *
46
+ * When `transitional` is false or the API is unavailable:
47
+ * - Executes the mutation directly (no animation)
48
+ *
49
+ * @param {Node} markerNode - The DOM node used as state key
50
+ * @param {'show' | 'hide'} direction - Determines which pending flag to check
51
+ * @param {boolean} transitional - Whether to use view transitions
52
+ * @param {() => void} domMutation - The function that performs the actual DOM changes
53
+ */
54
+ export function withTransition(markerNode, direction, transitional, domMutation) {
55
+ if (!transitional || typeof document === 'undefined' || !document.startViewTransition) {
56
+ domMutation();
57
+ return;
58
+ }
59
+
60
+ const state = getTransitionState(markerNode);
61
+
62
+ if (direction === 'show') {
63
+ state.hidePending = false;
64
+ if (state.showPending) return;
65
+ state.showPending = true;
66
+ state.active?.skipTransition();
67
+ state.active = document.startViewTransition(domMutation);
68
+ state.active.finished.finally(() => { state.showPending = false; });
69
+ } else {
70
+ state.showPending = false;
71
+ if (state.hidePending) return;
72
+ state.hidePending = true;
73
+ state.active?.skipTransition();
74
+ state.active = document.startViewTransition(domMutation);
75
+ state.active.finished.finally(() => { state.hidePending = false; });
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Track which rootNodes already have the hide style injected.
81
+ */
82
+ const styleInjected = new WeakSet();
83
+
84
+ /**
85
+ * Default CSS class name for hidden elements during transitions.
86
+ */
87
+ export const DEFAULT_HIDE_CLASS = 'ag-hide';
88
+
89
+ /**
90
+ * Ensure the hide class style is injected into the rootNode (once per rootNode).
91
+ *
92
+ * @param {any} rootNode - The Document, ShadowRoot, or element root to inject into
93
+ * @param {string} [hideClass='ag-hide'] - CSS class name
94
+ * @param {string} [hideCss='display: none'] - CSS properties for the hide class
95
+ */
96
+ export function ensureHideStyle(rootNode, hideClass = DEFAULT_HIDE_CLASS, hideCss = 'display: none') {
97
+ // Determine the injection target (shadowRoot or document.head)
98
+ let target = rootNode;
99
+ if (target.host === undefined && typeof document !== 'undefined') {
100
+ target = document.head;
101
+ }
102
+
103
+ if (styleInjected.has(target)) return;
104
+ styleInjected.add(target);
105
+
106
+ const style = document.createElement('style');
107
+ style.textContent = `.${hideClass} { ${hideCss} }`;
108
+ target.appendChild(style);
109
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * transitionHelper.ts — Shared view transition coordination utility.
3
+ *
4
+ * Provides transition state management, the startViewTransition wrapper with
5
+ * cancel/re-entry protection, and one-time CSS style injection.
6
+ *
7
+ * Used by lazyLoad, lazyLoadSwitch, and available for external consumers
8
+ * like be-switched.
9
+ *
10
+ * @example
11
+ * import { withTransition, ensureHideStyle } from 'assign-gingerly/transitionHelper.js';
12
+ *
13
+ * ensureHideStyle(rootNode); // inject default .ag-hide style once
14
+ *
15
+ * withTransition(markerNode, 'show', true, () => {
16
+ * element.classList.remove('ag-hide');
17
+ * });
18
+ */
19
+
20
+ /**
21
+ * Transition state for a given DOM marker node.
22
+ */
23
+ export interface TransitionState {
24
+ active?: ViewTransition;
25
+ showPending: boolean;
26
+ hidePending: boolean;
27
+ }
28
+
29
+ /**
30
+ * Module-level state map: marker node → transition state.
31
+ * WeakMap ensures cleanup when markers are GC'd.
32
+ */
33
+ const stateMap = new WeakMap<Node, TransitionState>();
34
+
35
+ /**
36
+ * Get or create transition state for a marker node.
37
+ */
38
+ export function getTransitionState(markerNode: Node): TransitionState {
39
+ let state = stateMap.get(markerNode);
40
+ if (!state) {
41
+ state = { showPending: false, hidePending: false };
42
+ stateMap.set(markerNode, state);
43
+ }
44
+ return state;
45
+ }
46
+
47
+ /**
48
+ * Execute a DOM mutation with optional view transition coordination.
49
+ *
50
+ * When `transitional` is true and `document.startViewTransition` is available:
51
+ * - Cancels any in-flight transition for this marker
52
+ * - Prevents re-entry (duplicate show/hide while one is pending)
53
+ * - Wraps the mutation in `document.startViewTransition`
54
+ *
55
+ * When `transitional` is false or the API is unavailable:
56
+ * - Executes the mutation directly (no animation)
57
+ *
58
+ * @param markerNode - The DOM node used as state key (typically the start comment marker)
59
+ * @param direction - 'show' or 'hide' — determines which pending flag to check
60
+ * @param transitional - Whether to use view transitions
61
+ * @param domMutation - The function that performs the actual DOM changes
62
+ */
63
+ export function withTransition(
64
+ markerNode: Node,
65
+ direction: 'show' | 'hide',
66
+ transitional: boolean,
67
+ domMutation: () => void
68
+ ): void {
69
+ if (!transitional || typeof document === 'undefined' || !document.startViewTransition) {
70
+ domMutation();
71
+ return;
72
+ }
73
+
74
+ const state = getTransitionState(markerNode);
75
+
76
+ if (direction === 'show') {
77
+ state.hidePending = false;
78
+ if (state.showPending) return;
79
+ state.showPending = true;
80
+ state.active?.skipTransition();
81
+ state.active = document.startViewTransition(domMutation);
82
+ state.active.finished.finally(() => { state.showPending = false; });
83
+ } else {
84
+ state.showPending = false;
85
+ if (state.hidePending) return;
86
+ state.hidePending = true;
87
+ state.active?.skipTransition();
88
+ state.active = document.startViewTransition(domMutation);
89
+ state.active.finished.finally(() => { state.hidePending = false; });
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Track which rootNodes already have the hide style injected.
95
+ */
96
+ const styleInjected = new WeakSet<object>();
97
+
98
+ /**
99
+ * Default CSS class name for hidden elements during transitions.
100
+ */
101
+ export const DEFAULT_HIDE_CLASS = 'ag-hide';
102
+
103
+ /**
104
+ * Default CSS rule for the hide class.
105
+ */
106
+ const DEFAULT_HIDE_CSS = `display: none`;
107
+
108
+ /**
109
+ * Ensure the hide class style is injected into the rootNode (once per rootNode).
110
+ *
111
+ * @param rootNode - The Document, ShadowRoot, or element root to inject into
112
+ * @param hideClass - CSS class name (default: 'ag-hide')
113
+ * @param hideCss - CSS properties for the hide class (default: 'display: none')
114
+ */
115
+ export function ensureHideStyle(
116
+ rootNode: any,
117
+ hideClass: string = DEFAULT_HIDE_CLASS,
118
+ hideCss: string = DEFAULT_HIDE_CSS
119
+ ): void {
120
+ // Determine the injection target (shadowRoot or document.head)
121
+ let target = rootNode;
122
+ if (target.host === undefined && typeof document !== 'undefined') {
123
+ target = document.head;
124
+ }
125
+
126
+ if (styleInjected.has(target)) return;
127
+ styleInjected.add(target);
128
+
129
+ const style = document.createElement('style');
130
+ style.textContent = `.${hideClass} { ${hideCss} }`;
131
+ target.appendChild(style);
132
+ }
@@ -481,3 +481,80 @@ export declare class PropertyBag {
481
481
  customElementRegistry: any;
482
482
  constructor(hostElement: any, ctx?: FeatureSpawnContext, initVals?: any);
483
483
  }
484
+
485
+ // =============================================================================
486
+ // assignFrom handler types
487
+ // =============================================================================
488
+
489
+ /**
490
+ * Base configuration for an assignFrom handler invocation.
491
+ * The `do` field identifies the handler; `resolve` maps named parameters to path strings.
492
+ */
493
+ export interface HandlerConfig {
494
+ /** The registered handler name */
495
+ do: string;
496
+ /** Named parameters to resolve against the `from` source before passing to the handler */
497
+ resolve?: Record<string, string>;
498
+ }
499
+
500
+ /**
501
+ * Interface for assignFrom handler classes.
502
+ * Handlers are invoked when a LHS key ends with ' =>'.
503
+ */
504
+ export interface AssignFromHandler {
505
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any): Promise<void> | void;
506
+ }
507
+
508
+ /**
509
+ * Constructor signature for assignFrom handler classes.
510
+ */
511
+ export interface AssignFromHandlerConstructor {
512
+ new (config: HandlerConfig): AssignFromHandler;
513
+ }
514
+
515
+ // =============================================================================
516
+ // Built-in handler config types
517
+ // =============================================================================
518
+
519
+ /**
520
+ * Configuration for the builtIns.lazyLoad handler.
521
+ */
522
+ export interface LazyLoadConfig extends HandlerConfig {
523
+ do: 'builtIns.lazyLoad';
524
+ resolve: {
525
+ /** Condition to show/hide (resolved from VM) */
526
+ if: string;
527
+ /** Template element to clone (resolved via protocol or path) */
528
+ instantiate: string;
529
+ /** Insert method: 'appendChild' (default) or 'prepend' */
530
+ method?: string;
531
+ /** If true, removes nodes when hiding instead of adding hidden attribute */
532
+ forget?: boolean | string;
533
+ /** Optional async callback invoked after cloning, resolved from the VM */
534
+ onInstantiated?: string;
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Context passed to onInstantiated callbacks after template cloning.
540
+ */
541
+ export interface LazyLoadInstantiatedContext {
542
+ /** The inserted child nodes */
543
+ nodes: Node[];
544
+ /** The target element containing the markers */
545
+ target: Element;
546
+ /** The full handler config */
547
+ config: any;
548
+ /** The resolved parameters */
549
+ resolvedParams: Record<string, any>;
550
+ }
551
+
552
+ /**
553
+ * The LazyLoadHandler class (exported for subclassing).
554
+ */
555
+ export declare class LazyLoadHandler implements AssignFromHandler {
556
+ config: any;
557
+ constructor(config: any);
558
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any): Promise<void>;
559
+ protected onCloneInserted(nodes: Node[], lhsTarget: Element, resolvedParams: Record<string, any>): Promise<void>;
560
+ }