assign-gingerly 0.0.55 → 0.0.56

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.
@@ -19,8 +19,7 @@
19
19
  * }, { withMethods: ['querySelector'], from: myVM });
20
20
  */
21
21
  import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
22
- const MARKER_START_PREFIX = '?start name="';
23
- const MARKER_END = '?end';
22
+ import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling } from '../markerUtils.js';
24
23
  /**
25
24
  * Get the marker name from a template element (uses its id).
26
25
  */
@@ -33,55 +32,6 @@ function getMarkerName(templateEl) {
33
32
  }
34
33
  return 'anonymous';
35
34
  }
36
- /**
37
- * Find existing start/end comment markers in a target element.
38
- * Returns [startMarker, endMarker] or [null, null] if not found.
39
- */
40
- function findMarkers(target, name) {
41
- const startText = `${MARKER_START_PREFIX}${name}"`;
42
- let startMarker = null;
43
- let endMarker = null;
44
- const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
45
- let node;
46
- while ((node = walker.nextNode())) {
47
- if (!startMarker && node.data === startText) {
48
- startMarker = node;
49
- }
50
- else if (startMarker && !endMarker && node.data === MARKER_END) {
51
- endMarker = node;
52
- break;
53
- }
54
- }
55
- return [startMarker, endMarker];
56
- }
57
- /**
58
- * Create start/end markers and insert them into the target.
59
- */
60
- function createMarkers(target, name, method) {
61
- const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
62
- const endMarker = document.createComment(MARKER_END);
63
- if (method === 'prepend') {
64
- target.prepend(endMarker);
65
- target.prepend(startMarker);
66
- }
67
- else {
68
- target.appendChild(startMarker);
69
- target.appendChild(endMarker);
70
- }
71
- return [startMarker, endMarker];
72
- }
73
- /**
74
- * Get all nodes between start and end markers.
75
- */
76
- function getNodesBetweenMarkers(start, end) {
77
- const nodes = [];
78
- let current = start.nextSibling;
79
- while (current && current !== end) {
80
- nodes.push(current);
81
- current = current.nextSibling;
82
- }
83
- return nodes;
84
- }
85
35
  /**
86
36
  * LazyLoadHandler — built-in handler for conditional template instantiation.
87
37
  *
@@ -93,26 +43,20 @@ export class LazyLoadHandler {
93
43
  this.config = config;
94
44
  }
95
45
  async assign(lhsTarget, resolvedParams) {
96
- const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS } = resolvedParams;
46
+ const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS, markerName, toggleInert = false, toggleDisabled = false } = resolvedParams;
97
47
  if (!(lhsTarget instanceof Element)) {
98
48
  throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
99
49
  }
100
- const name = getMarkerName(instantiate);
101
- let [startMarker, endMarker] = findMarkers(lhsTarget, name);
50
+ const name = markerName ?? getMarkerName(instantiate) ?? (lhsTarget.id || 'anonymous');
51
+ let [startMarker, endMarker] = method === 'after'
52
+ ? findMarkersSibling(lhsTarget, name)
53
+ : findMarkers(lhsTarget, name);
102
54
  if (condition) {
103
55
  if (startMarker && endMarker) {
104
56
  const nodes = getNodesBetweenMarkers(startMarker, endMarker);
105
57
  if (nodes.length > 0) {
106
58
  withTransition(startMarker, 'show', transitional, () => {
107
- for (const node of nodes) {
108
- if (node instanceof Element) {
109
- if (transitional) {
110
- node.classList.remove(hideClass);
111
- } else {
112
- node.removeAttribute('hidden');
113
- }
114
- }
115
- }
59
+ this.showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled);
116
60
  });
117
61
  }
118
62
  else {
@@ -127,7 +71,11 @@ export class LazyLoadHandler {
127
71
  }
128
72
  }
129
73
  else {
130
- [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
74
+ if (method === 'after') {
75
+ [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
76
+ } else {
77
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
78
+ }
131
79
  if (transitional) {
132
80
  ensureHideStyle(lhsTarget.getRootNode());
133
81
  withTransition(startMarker, 'show', true, () => {
@@ -151,21 +99,47 @@ export class LazyLoadHandler {
151
99
  }
152
100
  else {
153
101
  withTransition(startMarker, 'hide', transitional, () => {
154
- for (const node of nodes) {
155
- if (node instanceof Element) {
156
- if (transitional) {
157
- ensureHideStyle(lhsTarget.getRootNode(), hideClass);
158
- node.classList.add(hideClass);
159
- } else {
160
- node.setAttribute('hidden', '');
161
- }
162
- }
163
- }
102
+ this.hideNodes(nodes, lhsTarget, transitional, hideClass, toggleInert, toggleDisabled);
164
103
  });
165
104
  }
166
105
  }
167
106
  }
168
107
  }
108
+ showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled) {
109
+ for (const node of nodes) {
110
+ if (node instanceof Element) {
111
+ if (transitional) {
112
+ node.classList.remove(hideClass);
113
+ } else {
114
+ node.removeAttribute('hidden');
115
+ }
116
+ if (toggleInert) {
117
+ node.removeAttribute('inert');
118
+ }
119
+ if (toggleDisabled && 'disabled' in node) {
120
+ node.disabled = false;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ hideNodes(nodes, lhsTarget, transitional, hideClass, toggleInert, toggleDisabled) {
126
+ for (const node of nodes) {
127
+ if (node instanceof Element) {
128
+ if (transitional) {
129
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass);
130
+ node.classList.add(hideClass);
131
+ } else {
132
+ node.setAttribute('hidden', '');
133
+ }
134
+ if (toggleInert) {
135
+ node.setAttribute('inert', '');
136
+ }
137
+ if (toggleDisabled && 'disabled' in node) {
138
+ node.disabled = true;
139
+ }
140
+ }
141
+ }
142
+ }
169
143
  cloneAndInsertSync(templateEl, startMarker, endMarker, lhsTarget, resolvedParams) {
170
144
  let content;
171
145
  if (templateEl instanceof HTMLTemplateElement) {
@@ -20,24 +20,11 @@
20
20
  */
21
21
 
22
22
  import type { AssignFromHandler } from '../assignFrom.js';
23
+ import type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
23
24
  import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
25
+ import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
24
26
 
25
- const MARKER_START_PREFIX = '?start name="';
26
- const MARKER_END = '?end';
27
-
28
- /**
29
- * Context passed to onInstantiated callbacks.
30
- */
31
- export interface LazyLoadInstantiatedContext {
32
- /** The inserted child nodes */
33
- nodes: Node[];
34
- /** The target element containing the markers */
35
- target: Element;
36
- /** The full handler config */
37
- config: any;
38
- /** The resolved parameters */
39
- resolvedParams: Record<string, any>;
40
- }
27
+ export type { LazyLoadResolvedParams, LazyLoadInstantiatedContext };
41
28
 
42
29
  /**
43
30
  * Get the marker name from a template element (uses its id).
@@ -52,61 +39,6 @@ function getMarkerName(templateEl: any): string {
52
39
  return 'anonymous';
53
40
  }
54
41
 
55
- /**
56
- * Find existing start/end comment markers in a target element.
57
- * Returns [startMarker, endMarker] or [null, null] if not found.
58
- */
59
- function findMarkers(target: Element, name: string): [Comment | null, Comment | null] {
60
- const startText = `${MARKER_START_PREFIX}${name}"`;
61
- let startMarker: Comment | null = null;
62
- let endMarker: Comment | null = null;
63
-
64
- const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
65
- let node: Comment | null;
66
- while ((node = walker.nextNode() as Comment | null)) {
67
- if (!startMarker && node.data === startText) {
68
- startMarker = node;
69
- } else if (startMarker && !endMarker && node.data === MARKER_END) {
70
- endMarker = node;
71
- break;
72
- }
73
- }
74
-
75
- return [startMarker, endMarker];
76
- }
77
-
78
- /**
79
- * Create start/end markers and insert them into the target.
80
- */
81
- function createMarkers(target: Element, name: string, method: string): [Comment, Comment] {
82
- const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
83
- const endMarker = document.createComment(MARKER_END);
84
-
85
- if (method === 'prepend') {
86
- target.prepend(endMarker);
87
- target.prepend(startMarker);
88
- } else {
89
- // Default: appendChild
90
- target.appendChild(startMarker);
91
- target.appendChild(endMarker);
92
- }
93
-
94
- return [startMarker, endMarker];
95
- }
96
-
97
- /**
98
- * Get all nodes between start and end markers.
99
- */
100
- function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
101
- const nodes: Node[] = [];
102
- let current: Node | null = start.nextSibling;
103
- while (current && current !== end) {
104
- nodes.push(current);
105
- current = current.nextSibling;
106
- }
107
- return nodes;
108
- }
109
-
110
42
  /**
111
43
  * LazyLoadHandler — built-in handler for conditional template instantiation.
112
44
  *
@@ -119,7 +51,7 @@ export class LazyLoadHandler implements AssignFromHandler {
119
51
  this.config = config;
120
52
  }
121
53
 
122
- async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
54
+ async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams): Promise<void> {
123
55
  const {
124
56
  if: condition,
125
57
  instantiate,
@@ -127,42 +59,36 @@ export class LazyLoadHandler implements AssignFromHandler {
127
59
  forget = false,
128
60
  transitional = false,
129
61
  hideClass = DEFAULT_HIDE_CLASS,
62
+ markerName,
63
+ toggleInert = false,
64
+ toggleDisabled = false,
130
65
  } = resolvedParams;
131
66
 
132
67
  if (!(lhsTarget instanceof Element)) {
133
68
  throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
134
69
  }
135
70
 
136
- const name = getMarkerName(instantiate);
71
+ const name = markerName ?? getMarkerName(instantiate) ?? (lhsTarget.id || 'anonymous');
137
72
 
138
- // Find or create markers
139
- let [startMarker, endMarker] = findMarkers(lhsTarget, name);
73
+ // Find or create markers based on method
74
+ let [startMarker, endMarker] = method === 'after'
75
+ ? findMarkersSibling(lhsTarget, name)
76
+ : findMarkers(lhsTarget, name);
140
77
 
141
78
  if (condition) {
142
79
  // SHOW
143
80
  if (startMarker && endMarker) {
144
- // Markers exist — check if content is hidden or removed
145
81
  const nodes = getNodesBetweenMarkers(startMarker, endMarker);
146
82
  if (nodes.length > 0) {
147
83
  // Content exists — show it
148
84
  withTransition(startMarker, 'show', transitional, () => {
149
- for (const node of nodes) {
150
- if (node instanceof Element) {
151
- if (transitional) {
152
- node.classList.remove(hideClass);
153
- } else {
154
- node.removeAttribute('hidden');
155
- }
156
- }
157
- }
85
+ this.showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled);
158
86
  });
159
87
  } else {
160
88
  // Content was removed (forget mode) — re-clone
161
89
  if (transitional) {
162
90
  ensureHideStyle(lhsTarget.getRootNode());
163
91
  withTransition(startMarker, 'show', true, () => {
164
- // cloneAndInsert is async but the transition callback is sync
165
- // For transitions with clone, we insert synchronously
166
92
  this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
167
93
  });
168
94
  } else {
@@ -171,7 +97,11 @@ export class LazyLoadHandler implements AssignFromHandler {
171
97
  }
172
98
  } else {
173
99
  // No markers — first time. Create markers and clone template.
174
- [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
100
+ if (method === 'after') {
101
+ [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
102
+ } else {
103
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
104
+ }
175
105
  if (transitional) {
176
106
  ensureHideStyle(lhsTarget.getRootNode());
177
107
  withTransition(startMarker, 'show', true, () => {
@@ -188,29 +118,73 @@ export class LazyLoadHandler implements AssignFromHandler {
188
118
  if (nodes.length === 0) return;
189
119
 
190
120
  if (forget) {
191
- // Remove nodes entirely (markers persist for re-insertion)
192
121
  withTransition(startMarker, 'hide', transitional, () => {
193
122
  for (const node of nodes) {
194
123
  node.parentNode?.removeChild(node);
195
124
  }
196
125
  });
197
126
  } else {
198
- // Hide nodes
199
127
  withTransition(startMarker, 'hide', transitional, () => {
200
- for (const node of nodes) {
201
- if (node instanceof Element) {
202
- if (transitional) {
203
- ensureHideStyle(lhsTarget.getRootNode(), hideClass);
204
- node.classList.add(hideClass);
205
- } else {
206
- node.setAttribute('hidden', '');
207
- }
208
- }
209
- }
128
+ this.hideNodes(nodes, lhsTarget, transitional, hideClass, toggleInert, toggleDisabled);
210
129
  });
211
130
  }
212
131
  }
213
- // If no markers exist and condition is false, do nothing (never loaded)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Show nodes — remove hidden/class and restore inert/disabled state.
137
+ */
138
+ protected showNodes(
139
+ nodes: Node[],
140
+ transitional: boolean,
141
+ hideClass: string,
142
+ toggleInert: boolean,
143
+ toggleDisabled: boolean
144
+ ): void {
145
+ for (const node of nodes) {
146
+ if (node instanceof Element) {
147
+ if (transitional) {
148
+ node.classList.remove(hideClass);
149
+ } else {
150
+ node.removeAttribute('hidden');
151
+ }
152
+ if (toggleInert) {
153
+ node.removeAttribute('inert');
154
+ }
155
+ if (toggleDisabled && 'disabled' in node) {
156
+ (node as any).disabled = false;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Hide nodes — add hidden/class and set inert/disabled state.
164
+ */
165
+ protected hideNodes(
166
+ nodes: Node[],
167
+ lhsTarget: Element,
168
+ transitional: boolean,
169
+ hideClass: string,
170
+ toggleInert: boolean,
171
+ toggleDisabled: boolean
172
+ ): void {
173
+ for (const node of nodes) {
174
+ if (node instanceof Element) {
175
+ if (transitional) {
176
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass);
177
+ node.classList.add(hideClass);
178
+ } else {
179
+ node.setAttribute('hidden', '');
180
+ }
181
+ if (toggleInert) {
182
+ node.setAttribute('inert', '');
183
+ }
184
+ if (toggleDisabled && 'disabled' in node) {
185
+ (node as any).disabled = true;
186
+ }
187
+ }
214
188
  }
215
189
  }
216
190
 
@@ -224,7 +198,7 @@ export class LazyLoadHandler implements AssignFromHandler {
224
198
  startMarker: Comment,
225
199
  endMarker: Comment,
226
200
  lhsTarget: Element,
227
- resolvedParams: Record<string, any>
201
+ resolvedParams: LazyLoadResolvedParams
228
202
  ): Node[] {
229
203
  let content: DocumentFragment;
230
204
 
@@ -252,7 +226,7 @@ export class LazyLoadHandler implements AssignFromHandler {
252
226
  startMarker: Comment,
253
227
  endMarker: Comment,
254
228
  lhsTarget: Element,
255
- resolvedParams: Record<string, any>
229
+ resolvedParams: LazyLoadResolvedParams
256
230
  ): Promise<Node[]> {
257
231
  let content: DocumentFragment;
258
232
 
@@ -300,7 +274,7 @@ export class LazyLoadHandler implements AssignFromHandler {
300
274
  protected async onCloneInserted(
301
275
  nodes: Node[],
302
276
  lhsTarget: Element,
303
- resolvedParams: Record<string, any>
277
+ resolvedParams: LazyLoadResolvedParams
304
278
  ): Promise<void> {
305
279
  // No-op by default. Subclasses override.
306
280
  }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { LazyLoadHandler } from './lazyLoad.js';
24
24
  import type { AssignFromHandler } from '../assignFrom.js';
25
+ import type { LazyLoadSwitchResolvedParams } from '../types/assign-gingerly/types.js';
25
26
 
26
27
  /**
27
28
  * Evaluate a comparison operation.
@@ -54,7 +55,7 @@ function evaluateOp(lhs: any, op: string, rhs: any): boolean {
54
55
  */
55
56
  export class LazyLoadSwitchHandler extends LazyLoadHandler implements AssignFromHandler {
56
57
 
57
- async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
58
+ async assign(lhsTarget: any, resolvedParams: LazyLoadSwitchResolvedParams): Promise<void> {
58
59
  const { lhs, op = '===', rhs, ...rest } = resolvedParams;
59
60
  const condition = evaluateOp(lhs, op, rhs);
60
61
  // Delegate to parent with computed condition
package/markerUtils.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * markerUtils.js — Shared utilities for comment marker management.
3
+ *
4
+ * Used by lazyLoad, microDataJoin, and future template loop handlers.
5
+ */
6
+
7
+ export const MARKER_START_PREFIX = '?start name="';
8
+ export const MARKER_END = '?end';
9
+
10
+ /**
11
+ * Find existing start/end comment markers (TreeWalker approach).
12
+ */
13
+ export function findMarkers(target, name) {
14
+ const startText = `${MARKER_START_PREFIX}${name}"`;
15
+ let startMarker = null;
16
+ let endMarker = null;
17
+
18
+ const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
19
+ let node;
20
+ while ((node = walker.nextNode())) {
21
+ if (!startMarker && node.data === startText) {
22
+ startMarker = node;
23
+ } else if (startMarker && !endMarker && node.data === MARKER_END) {
24
+ endMarker = node;
25
+ break;
26
+ }
27
+ }
28
+
29
+ return [startMarker, endMarker];
30
+ }
31
+
32
+ /**
33
+ * Find existing start/end comment markers (XPath approach).
34
+ */
35
+ export function findMarkersXPath(target, name) {
36
+ const startText = `${MARKER_START_PREFIX}${name}"`;
37
+
38
+ const startResult = document.evaluate(
39
+ `.//comment()[. = "${startText}"]`,
40
+ target,
41
+ null,
42
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
43
+ null
44
+ );
45
+ const startMarker = startResult.singleNodeValue;
46
+ if (!startMarker) return [null, null];
47
+
48
+ const endResult = document.evaluate(
49
+ `following-sibling::comment()[. = "${MARKER_END}"][1]`,
50
+ startMarker,
51
+ null,
52
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
53
+ null
54
+ );
55
+ const endMarker = endResult.singleNodeValue;
56
+
57
+ return [startMarker, endMarker];
58
+ }
59
+
60
+ /**
61
+ * Create start/end markers and insert them into the target.
62
+ */
63
+ export function createMarkers(target, name, method = 'appendChild') {
64
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
65
+ const endMarker = document.createComment(MARKER_END);
66
+
67
+ if (method === 'prepend') {
68
+ target.prepend(endMarker);
69
+ target.prepend(startMarker);
70
+ } else {
71
+ target.appendChild(startMarker);
72
+ target.appendChild(endMarker);
73
+ }
74
+
75
+ return [startMarker, endMarker];
76
+ }
77
+
78
+ /**
79
+ * Get all nodes between start and end markers.
80
+ */
81
+ export function getNodesBetweenMarkers(start, end) {
82
+ const nodes = [];
83
+ let current = start.nextSibling;
84
+ while (current && current !== end) {
85
+ nodes.push(current);
86
+ current = current.nextSibling;
87
+ }
88
+ return nodes;
89
+ }
90
+
91
+ /**
92
+ * Find existing start/end comment markers among siblings of an anchor element.
93
+ * Used for 'after' insertion mode.
94
+ */
95
+ export function findMarkersSibling(anchor, name) {
96
+ const startText = `${MARKER_START_PREFIX}${name}"`;
97
+ let startMarker = null;
98
+ let endMarker = null;
99
+
100
+ let current = anchor.nextSibling;
101
+ while (current) {
102
+ if (current.nodeType === Node.COMMENT_NODE) {
103
+ if (!startMarker && current.data === startText) {
104
+ startMarker = current;
105
+ } else if (startMarker && !endMarker && current.data === MARKER_END) {
106
+ endMarker = current;
107
+ break;
108
+ }
109
+ }
110
+ current = current.nextSibling;
111
+ }
112
+
113
+ return [startMarker, endMarker];
114
+ }
115
+
116
+ /**
117
+ * Create start/end markers as siblings after an anchor element.
118
+ * Used for 'after' insertion mode.
119
+ */
120
+ export function createMarkersSibling(anchor, name) {
121
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
122
+ const endMarker = document.createComment(MARKER_END);
123
+
124
+ anchor.after(startMarker, endMarker);
125
+
126
+ return [startMarker, endMarker];
127
+ }
package/markerUtils.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * markerUtils.ts — Shared utilities for comment marker management.
3
+ *
4
+ * Used by lazyLoad, microDataJoin, and future template loop handlers.
5
+ * Provides finding, creating, and traversing comment marker pairs.
6
+ *
7
+ * Markers are HTML comment nodes with specific content:
8
+ * - Start: <!--?start name="markerName"-->
9
+ * - End: <!--?end-->
10
+ */
11
+
12
+ export const MARKER_START_PREFIX = '?start name="';
13
+ export const MARKER_END = '?end';
14
+
15
+ /**
16
+ * Find existing start/end comment markers in a target element (TreeWalker approach).
17
+ * Searches the subtree of `target` for matching comment nodes.
18
+ *
19
+ * @param target - The element to search within
20
+ * @param name - The marker name to find
21
+ * @returns [startMarker, endMarker] or [null, null] if not found
22
+ */
23
+ export function findMarkers(target: Element | Node, name: string): [Comment | null, Comment | null] {
24
+ const startText = `${MARKER_START_PREFIX}${name}"`;
25
+ let startMarker: Comment | null = null;
26
+ let endMarker: Comment | null = null;
27
+
28
+ const walker = document.createTreeWalker(target as Node, NodeFilter.SHOW_COMMENT);
29
+ let node: Comment | null;
30
+ while ((node = walker.nextNode() as Comment | null)) {
31
+ if (!startMarker && node.data === startText) {
32
+ startMarker = node;
33
+ } else if (startMarker && !endMarker && node.data === MARKER_END) {
34
+ endMarker = node;
35
+ break;
36
+ }
37
+ }
38
+
39
+ return [startMarker, endMarker];
40
+ }
41
+
42
+ /**
43
+ * Find existing start/end comment markers using XPath (alternative approach).
44
+ * May be faster in large DOMs due to engine-level indexing.
45
+ *
46
+ * @param target - The element to search within
47
+ * @param name - The marker name to find
48
+ * @returns [startMarker, endMarker] or [null, null] if not found
49
+ */
50
+ export function findMarkersXPath(target: Element | Node, name: string): [Comment | null, Comment | null] {
51
+ const startText = `${MARKER_START_PREFIX}${name}"`;
52
+
53
+ const startResult = document.evaluate(
54
+ `.//comment()[. = "${startText}"]`,
55
+ target,
56
+ null,
57
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
58
+ null
59
+ );
60
+ const startMarker = startResult.singleNodeValue as Comment | null;
61
+ if (!startMarker) return [null, null];
62
+
63
+ // Find the next sibling comment that is the end marker
64
+ const endResult = document.evaluate(
65
+ `following-sibling::comment()[. = "${MARKER_END}"][1]`,
66
+ startMarker,
67
+ null,
68
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
69
+ null
70
+ );
71
+ const endMarker = endResult.singleNodeValue as Comment | null;
72
+
73
+ return [startMarker, endMarker];
74
+ }
75
+
76
+ /**
77
+ * Create start/end markers and insert them into the target.
78
+ *
79
+ * @param target - The element to insert markers into
80
+ * @param name - The marker name
81
+ * @param method - 'appendChild' (default) or 'prepend'
82
+ * @returns [startMarker, endMarker]
83
+ */
84
+ export function createMarkers(target: Element, name: string, method: string = 'appendChild'): [Comment, Comment] {
85
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
86
+ const endMarker = document.createComment(MARKER_END);
87
+
88
+ if (method === 'prepend') {
89
+ target.prepend(endMarker);
90
+ target.prepend(startMarker);
91
+ } else {
92
+ target.appendChild(startMarker);
93
+ target.appendChild(endMarker);
94
+ }
95
+
96
+ return [startMarker, endMarker];
97
+ }
98
+
99
+ /**
100
+ * Get all nodes between start and end markers.
101
+ *
102
+ * @param start - The start comment marker
103
+ * @param end - The end comment marker
104
+ * @returns Array of nodes between the markers (exclusive of markers themselves)
105
+ */
106
+ export function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
107
+ const nodes: Node[] = [];
108
+ let current: Node | null = start.nextSibling;
109
+ while (current && current !== end) {
110
+ nodes.push(current);
111
+ current = current.nextSibling;
112
+ }
113
+ return nodes;
114
+ }
115
+
116
+ /**
117
+ * Find existing start/end comment markers among siblings of an anchor element.
118
+ * Used for 'after' insertion mode where markers are siblings, not children.
119
+ *
120
+ * @param anchor - The element after which markers were inserted
121
+ * @param name - The marker name to find
122
+ * @returns [startMarker, endMarker] or [null, null] if not found
123
+ */
124
+ export function findMarkersSibling(anchor: Element | Node, name: string): [Comment | null, Comment | null] {
125
+ const startText = `${MARKER_START_PREFIX}${name}"`;
126
+ let startMarker: Comment | null = null;
127
+ let endMarker: Comment | null = null;
128
+
129
+ let current: Node | null = anchor.nextSibling;
130
+ while (current) {
131
+ if (current.nodeType === Node.COMMENT_NODE) {
132
+ const comment = current as Comment;
133
+ if (!startMarker && comment.data === startText) {
134
+ startMarker = comment;
135
+ } else if (startMarker && !endMarker && comment.data === MARKER_END) {
136
+ endMarker = comment;
137
+ break;
138
+ }
139
+ }
140
+ current = current.nextSibling;
141
+ }
142
+
143
+ return [startMarker, endMarker];
144
+ }
145
+
146
+ /**
147
+ * Create start/end markers as siblings after an anchor element.
148
+ * Used for 'after' insertion mode.
149
+ *
150
+ * @param anchor - The element to insert markers after
151
+ * @param name - The marker name
152
+ * @returns [startMarker, endMarker]
153
+ */
154
+ export function createMarkersSibling(anchor: Element | Node, name: string): [Comment, Comment] {
155
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
156
+ const endMarker = document.createComment(MARKER_END);
157
+
158
+ // Insert after the anchor: anchor → startMarker → endMarker
159
+ (anchor as ChildNode).after(startMarker, endMarker);
160
+
161
+ return [startMarker, endMarker];
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.55",
3
+ "version": "0.0.56",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -110,6 +110,10 @@
110
110
  "default": "./isAllowedImportPath.js",
111
111
  "types": "./isAllowedImportPath.ts"
112
112
  },
113
+ "./markerUtils.js": {
114
+ "default": "./markerUtils.js",
115
+ "types": "./markerUtils.ts"
116
+ },
113
117
  "./inferredAssignments.js": {
114
118
  "default": "./inferredAssignments.js",
115
119
  "types": "./inferredAssignments.ts"
@@ -538,15 +538,90 @@ export interface LazyLoadConfig extends HandlerConfig {
538
538
  if: string;
539
539
  /** Template element to clone (resolved via protocol or path) */
540
540
  instantiate: string;
541
+ /** Insert method: 'appendChild' (default), 'prepend', or 'after' (sibling after target) */
542
+ method?: string;
543
+ /** If true, removes nodes when hiding instead of adding hidden attribute */
544
+ forget?: boolean | string;
545
+ /** Enable view transitions */
546
+ transitional?: boolean | string;
547
+ /** CSS class for hiding (default: 'ag-hide', only used when transitional: true) */
548
+ hideClass?: string;
549
+ /** Optional async callback invoked after cloning, resolved from the VM */
550
+ onInstantiated?: string;
551
+ /** Override auto-derived marker name */
552
+ markerName?: string;
553
+ /** Set inert attribute on hidden elements */
554
+ toggleInert?: boolean | string;
555
+ /** Set disabled property on hidden form elements */
556
+ toggleDisabled?: boolean | string;
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Resolved parameters received by LazyLoadHandler.assign() after resolveValues processing.
562
+ */
563
+ export interface LazyLoadResolvedParams {
564
+ /** Condition — resolved to actual truthy/falsy value */
565
+ if: any;
566
+ /** Template element — resolved to HTMLTemplateElement or DocumentFragment */
567
+ instantiate: HTMLTemplateElement | DocumentFragment;
568
+ /** Insertion method (default: 'appendChild') */
569
+ method?: 'appendChild' | 'prepend' | 'after';
570
+ /** Remove nodes on hide instead of using hidden attribute */
571
+ forget?: boolean;
572
+ /** Enable view transitions */
573
+ transitional?: boolean;
574
+ /** CSS class for hiding (default: 'ag-hide', only used when transitional: true) */
575
+ hideClass?: string;
576
+ /** Callback after clone+insert */
577
+ onInstantiated?: (ctx: LazyLoadInstantiatedContext) => void | Promise<void>;
578
+ /** Override auto-derived marker name */
579
+ markerName?: string;
580
+ /** Set inert attribute on hidden elements (removes from a11y tree + interaction) */
581
+ toggleInert?: boolean;
582
+ /** Set disabled property on hidden form elements */
583
+ toggleDisabled?: boolean;
584
+ }
585
+
586
+ /**
587
+ * Configuration for the builtIns.lazyLoadSwitch handler.
588
+ */
589
+ export interface LazyLoadSwitchConfig extends HandlerConfig {
590
+ do: 'builtIns.lazyLoadSwitch';
591
+ resolve: {
592
+ /** Left-hand side of comparison (resolved from VM) */
593
+ lhs: string;
594
+ /** Comparison operator (default: '===') */
595
+ op?: '===' | '!==' | '==' | '!=' | '<' | '>' | '<=' | '>=';
596
+ /** Right-hand side of comparison (resolved from VM or literal) */
597
+ rhs: string;
598
+ /** Template element to clone (resolved via protocol or path) */
599
+ instantiate: string;
541
600
  /** Insert method: 'appendChild' (default) or 'prepend' */
542
601
  method?: string;
543
602
  /** If true, removes nodes when hiding instead of adding hidden attribute */
544
603
  forget?: boolean | string;
604
+ /** Enable view transitions */
605
+ transitional?: boolean | string;
606
+ /** CSS class for hiding (default: 'ag-hide', only used when transitional: true) */
607
+ hideClass?: string;
545
608
  /** Optional async callback invoked after cloning, resolved from the VM */
546
609
  onInstantiated?: string;
547
610
  };
548
611
  }
549
612
 
613
+ /**
614
+ * Resolved parameters received by LazyLoadSwitchHandler.assign() after resolveValues processing.
615
+ */
616
+ export interface LazyLoadSwitchResolvedParams extends Omit<LazyLoadResolvedParams, 'if'> {
617
+ /** Left-hand side — resolved to actual value */
618
+ lhs: any;
619
+ /** Comparison operator (default: '===') */
620
+ op?: '===' | '!==' | '==' | '!=' | '<' | '>' | '<=' | '>=';
621
+ /** Right-hand side — resolved to actual value */
622
+ rhs: any;
623
+ }
624
+
550
625
  /**
551
626
  * Context passed to onInstantiated callbacks after template cloning.
552
627
  */