assign-gingerly 0.0.55 → 0.0.57

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
  *
@@ -89,30 +39,36 @@ function getNodesBetweenMarkers(start, end) {
89
39
  */
90
40
  export class LazyLoadHandler {
91
41
  config;
42
+ static #markerCounter = 0;
92
43
  constructor(config) {
93
44
  this.config = config;
94
45
  }
95
46
  async assign(lhsTarget, resolvedParams) {
96
- const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS } = resolvedParams;
47
+ const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS, hideCss, markerName, toggleInert = false, toggleDisabled = false } = resolvedParams;
97
48
  if (!(lhsTarget instanceof Element)) {
98
49
  throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
99
50
  }
100
- const name = getMarkerName(instantiate);
101
- let [startMarker, endMarker] = findMarkers(lhsTarget, name);
51
+ let name = markerName ?? getMarkerName(instantiate) ?? (lhsTarget.id || 'anonymous');
52
+ if (name === 'anonymous' && method === 'after') {
53
+ const stored = lhsTarget.dataset?.agMarker;
54
+ if (stored) {
55
+ name = stored;
56
+ } else {
57
+ name = `_ag_${LazyLoadHandler.#markerCounter++}`;
58
+ if (lhsTarget.dataset) {
59
+ lhsTarget.dataset.agMarker = name;
60
+ }
61
+ }
62
+ }
63
+ let [startMarker, endMarker] = method === 'after'
64
+ ? findMarkersSibling(lhsTarget, name)
65
+ : findMarkers(lhsTarget, name);
102
66
  if (condition) {
103
67
  if (startMarker && endMarker) {
104
68
  const nodes = getNodesBetweenMarkers(startMarker, endMarker);
105
69
  if (nodes.length > 0) {
106
70
  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
- }
71
+ this.showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled);
116
72
  });
117
73
  }
118
74
  else {
@@ -127,7 +83,11 @@ export class LazyLoadHandler {
127
83
  }
128
84
  }
129
85
  else {
130
- [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
86
+ if (method === 'after') {
87
+ [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
88
+ } else {
89
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
90
+ }
131
91
  if (transitional) {
132
92
  ensureHideStyle(lhsTarget.getRootNode());
133
93
  withTransition(startMarker, 'show', true, () => {
@@ -151,21 +111,47 @@ export class LazyLoadHandler {
151
111
  }
152
112
  else {
153
113
  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
- }
114
+ this.hideNodes(nodes, lhsTarget, transitional, hideClass, hideCss, toggleInert, toggleDisabled);
164
115
  });
165
116
  }
166
117
  }
167
118
  }
168
119
  }
120
+ showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled) {
121
+ for (const node of nodes) {
122
+ if (node instanceof Element) {
123
+ if (transitional) {
124
+ node.classList.remove(hideClass);
125
+ } else {
126
+ node.removeAttribute('hidden');
127
+ }
128
+ if (toggleInert) {
129
+ node.removeAttribute('inert');
130
+ }
131
+ if (toggleDisabled && 'disabled' in node) {
132
+ node.disabled = false;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ hideNodes(nodes, lhsTarget, transitional, hideClass, hideCss, toggleInert, toggleDisabled) {
138
+ for (const node of nodes) {
139
+ if (node instanceof Element) {
140
+ if (transitional) {
141
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
142
+ node.classList.add(hideClass);
143
+ } else {
144
+ node.setAttribute('hidden', '');
145
+ }
146
+ if (toggleInert) {
147
+ node.setAttribute('inert', '');
148
+ }
149
+ if (toggleDisabled && 'disabled' in node) {
150
+ node.disabled = true;
151
+ }
152
+ }
153
+ }
154
+ }
169
155
  cloneAndInsertSync(templateEl, startMarker, endMarker, lhsTarget, resolvedParams) {
170
156
  let content;
171
157
  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
  *
@@ -114,12 +46,13 @@ function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
114
46
  */
115
47
  export class LazyLoadHandler implements AssignFromHandler {
116
48
  config: any;
49
+ static #markerCounter = 0;
117
50
 
118
51
  constructor(config: any) {
119
52
  this.config = config;
120
53
  }
121
54
 
122
- async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
55
+ async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams): Promise<void> {
123
56
  const {
124
57
  if: condition,
125
58
  instantiate,
@@ -127,42 +60,50 @@ export class LazyLoadHandler implements AssignFromHandler {
127
60
  forget = false,
128
61
  transitional = false,
129
62
  hideClass = DEFAULT_HIDE_CLASS,
63
+ hideCss,
64
+ markerName,
65
+ toggleInert = false,
66
+ toggleDisabled = false,
130
67
  } = resolvedParams;
131
68
 
132
69
  if (!(lhsTarget instanceof Element)) {
133
70
  throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
134
71
  }
135
72
 
136
- const name = getMarkerName(instantiate);
73
+ // Determine marker name auto-generate for 'after' mode if would be 'anonymous'
74
+ let name = markerName ?? getMarkerName(instantiate) ?? (lhsTarget.id || 'anonymous');
75
+ if (name === 'anonymous' && method === 'after') {
76
+ // Check for previously stored name on the anchor
77
+ const stored = (lhsTarget as HTMLElement).dataset?.agMarker;
78
+ if (stored) {
79
+ name = stored;
80
+ } else {
81
+ name = `_ag_${LazyLoadHandler.#markerCounter++}`;
82
+ if ((lhsTarget as HTMLElement).dataset) {
83
+ (lhsTarget as HTMLElement).dataset.agMarker = name;
84
+ }
85
+ }
86
+ }
137
87
 
138
- // Find or create markers
139
- let [startMarker, endMarker] = findMarkers(lhsTarget, name);
88
+ // Find or create markers based on method
89
+ let [startMarker, endMarker] = method === 'after'
90
+ ? findMarkersSibling(lhsTarget, name)
91
+ : findMarkers(lhsTarget, name);
140
92
 
141
93
  if (condition) {
142
94
  // SHOW
143
95
  if (startMarker && endMarker) {
144
- // Markers exist — check if content is hidden or removed
145
96
  const nodes = getNodesBetweenMarkers(startMarker, endMarker);
146
97
  if (nodes.length > 0) {
147
98
  // Content exists — show it
148
99
  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
- }
100
+ this.showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled);
158
101
  });
159
102
  } else {
160
103
  // Content was removed (forget mode) — re-clone
161
104
  if (transitional) {
162
105
  ensureHideStyle(lhsTarget.getRootNode());
163
106
  withTransition(startMarker, 'show', true, () => {
164
- // cloneAndInsert is async but the transition callback is sync
165
- // For transitions with clone, we insert synchronously
166
107
  this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
167
108
  });
168
109
  } else {
@@ -171,7 +112,11 @@ export class LazyLoadHandler implements AssignFromHandler {
171
112
  }
172
113
  } else {
173
114
  // No markers — first time. Create markers and clone template.
174
- [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
115
+ if (method === 'after') {
116
+ [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
117
+ } else {
118
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
119
+ }
175
120
  if (transitional) {
176
121
  ensureHideStyle(lhsTarget.getRootNode());
177
122
  withTransition(startMarker, 'show', true, () => {
@@ -188,29 +133,74 @@ export class LazyLoadHandler implements AssignFromHandler {
188
133
  if (nodes.length === 0) return;
189
134
 
190
135
  if (forget) {
191
- // Remove nodes entirely (markers persist for re-insertion)
192
136
  withTransition(startMarker, 'hide', transitional, () => {
193
137
  for (const node of nodes) {
194
138
  node.parentNode?.removeChild(node);
195
139
  }
196
140
  });
197
141
  } else {
198
- // Hide nodes
199
142
  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
- }
143
+ this.hideNodes(nodes, lhsTarget, transitional, hideClass, hideCss, toggleInert, toggleDisabled);
210
144
  });
211
145
  }
212
146
  }
213
- // If no markers exist and condition is false, do nothing (never loaded)
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Show nodes — remove hidden/class and restore inert/disabled state.
152
+ */
153
+ protected showNodes(
154
+ nodes: Node[],
155
+ transitional: boolean,
156
+ hideClass: string,
157
+ toggleInert: boolean,
158
+ toggleDisabled: boolean
159
+ ): void {
160
+ for (const node of nodes) {
161
+ if (node instanceof Element) {
162
+ if (transitional) {
163
+ node.classList.remove(hideClass);
164
+ } else {
165
+ node.removeAttribute('hidden');
166
+ }
167
+ if (toggleInert) {
168
+ node.removeAttribute('inert');
169
+ }
170
+ if (toggleDisabled && 'disabled' in node) {
171
+ (node as any).disabled = false;
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Hide nodes — add hidden/class and set inert/disabled state.
179
+ */
180
+ protected hideNodes(
181
+ nodes: Node[],
182
+ lhsTarget: Element,
183
+ transitional: boolean,
184
+ hideClass: string,
185
+ hideCss: string | undefined,
186
+ toggleInert: boolean,
187
+ toggleDisabled: boolean
188
+ ): void {
189
+ for (const node of nodes) {
190
+ if (node instanceof Element) {
191
+ if (transitional) {
192
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
193
+ node.classList.add(hideClass);
194
+ } else {
195
+ node.setAttribute('hidden', '');
196
+ }
197
+ if (toggleInert) {
198
+ node.setAttribute('inert', '');
199
+ }
200
+ if (toggleDisabled && 'disabled' in node) {
201
+ (node as any).disabled = true;
202
+ }
203
+ }
214
204
  }
215
205
  }
216
206
 
@@ -224,7 +214,7 @@ export class LazyLoadHandler implements AssignFromHandler {
224
214
  startMarker: Comment,
225
215
  endMarker: Comment,
226
216
  lhsTarget: Element,
227
- resolvedParams: Record<string, any>
217
+ resolvedParams: LazyLoadResolvedParams
228
218
  ): Node[] {
229
219
  let content: DocumentFragment;
230
220
 
@@ -252,7 +242,7 @@ export class LazyLoadHandler implements AssignFromHandler {
252
242
  startMarker: Comment,
253
243
  endMarker: Comment,
254
244
  lhsTarget: Element,
255
- resolvedParams: Record<string, any>
245
+ resolvedParams: LazyLoadResolvedParams
256
246
  ): Promise<Node[]> {
257
247
  let content: DocumentFragment;
258
248
 
@@ -300,7 +290,7 @@ export class LazyLoadHandler implements AssignFromHandler {
300
290
  protected async onCloneInserted(
301
291
  nodes: Node[],
302
292
  lhsTarget: Element,
303
- resolvedParams: Record<string, any>
293
+ resolvedParams: LazyLoadResolvedParams
304
294
  ): Promise<void> {
305
295
  // No-op by default. Subclasses override.
306
296
  }
@@ -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.57",
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,96 @@ 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) or 'prepend' */
541
+ /** Insert method: 'appendChild' (default), 'prepend', or 'after' (sibling after target) */
542
542
  method?: string;
543
543
  /** If true, removes nodes when hiding instead of adding hidden attribute */
544
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
+ /** Custom CSS for the hide class (default: 'display: none') */
550
+ hideCss?: string;
545
551
  /** Optional async callback invoked after cloning, resolved from the VM */
546
552
  onInstantiated?: string;
553
+ /** Override auto-derived marker name */
554
+ markerName?: string;
555
+ /** Set inert attribute on hidden elements */
556
+ toggleInert?: boolean | string;
557
+ /** Set disabled property on hidden form elements */
558
+ toggleDisabled?: boolean | string;
547
559
  };
548
560
  }
549
561
 
562
+ /**
563
+ * Resolved parameters received by LazyLoadHandler.assign() after resolveValues processing.
564
+ */
565
+ export interface LazyLoadResolvedParams {
566
+ /** Condition — resolved to actual truthy/falsy value */
567
+ if: any;
568
+ /** Template element — resolved to HTMLTemplateElement or DocumentFragment */
569
+ instantiate: HTMLTemplateElement | DocumentFragment;
570
+ /** Insertion method (default: 'appendChild') */
571
+ method?: 'appendChild' | 'prepend' | 'after';
572
+ /** Remove nodes on hide instead of using hidden attribute */
573
+ forget?: boolean;
574
+ /** Enable view transitions */
575
+ transitional?: boolean;
576
+ /** CSS class for hiding (default: 'ag-hide', only used when transitional: true) */
577
+ hideClass?: string;
578
+ /** Custom CSS for the hide class (default: 'display: none') */
579
+ hideCss?: string;
580
+ /** Callback after clone+insert */
581
+ onInstantiated?: (ctx: LazyLoadInstantiatedContext) => void | Promise<void>;
582
+ /** Override auto-derived marker name */
583
+ markerName?: string;
584
+ /** Set inert attribute on hidden elements (removes from a11y tree + interaction) */
585
+ toggleInert?: boolean;
586
+ /** Set disabled property on hidden form elements */
587
+ toggleDisabled?: boolean;
588
+ }
589
+
590
+ /**
591
+ * Configuration for the builtIns.lazyLoadSwitch handler.
592
+ */
593
+ export interface LazyLoadSwitchConfig extends HandlerConfig {
594
+ do: 'builtIns.lazyLoadSwitch';
595
+ resolve: {
596
+ /** Left-hand side of comparison (resolved from VM) */
597
+ lhs: string;
598
+ /** Comparison operator (default: '===') */
599
+ op?: '===' | '!==' | '==' | '!=' | '<' | '>' | '<=' | '>=';
600
+ /** Right-hand side of comparison (resolved from VM or literal) */
601
+ rhs: string;
602
+ /** Template element to clone (resolved via protocol or path) */
603
+ instantiate: string;
604
+ /** Insert method: 'appendChild' (default), 'prepend', or 'after' */
605
+ method?: string;
606
+ /** If true, removes nodes when hiding instead of adding hidden attribute */
607
+ forget?: boolean | string;
608
+ /** Enable view transitions */
609
+ transitional?: boolean | string;
610
+ /** CSS class for hiding (default: 'ag-hide', only used when transitional: true) */
611
+ hideClass?: string;
612
+ /** Custom CSS for the hide class (default: 'display: none') */
613
+ hideCss?: string;
614
+ /** Optional async callback invoked after cloning, resolved from the VM */
615
+ onInstantiated?: string;
616
+ };
617
+ }
618
+
619
+ /**
620
+ * Resolved parameters received by LazyLoadSwitchHandler.assign() after resolveValues processing.
621
+ */
622
+ export interface LazyLoadSwitchResolvedParams extends Omit<LazyLoadResolvedParams, 'if'> {
623
+ /** Left-hand side — resolved to actual value */
624
+ lhs: any;
625
+ /** Comparison operator (default: '===') */
626
+ op?: '===' | '!==' | '==' | '!=' | '<' | '>' | '<=' | '>=';
627
+ /** Right-hand side — resolved to actual value */
628
+ rhs: any;
629
+ }
630
+
550
631
  /**
551
632
  * Context passed to onInstantiated callbacks after template cloning.
552
633
  */