assign-gingerly 0.0.59 → 0.0.60
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/README.md +269 -12
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +190 -12
- package/assignFrom.ts +184 -11
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +11 -9
- package/assignFromAsync.ts +27 -11
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +46 -1
- package/handlers/manageTemplateList.js +54 -31
- package/handlers/manageTemplateList.ts +51 -28
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +34 -4
- package/inferredAssignments.ts +56 -6
- package/package.json +13 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +6 -1
- package/processHandlerCommands.ts +7 -1
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/types/assign-gingerly/types.d.ts +10 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
package/assignFromAsync.ts
CHANGED
|
@@ -51,6 +51,17 @@ export interface AssignFromOptions extends IAssignGingerlyOptions {
|
|
|
51
51
|
*/
|
|
52
52
|
withIds?: Record<string, string | { qry: string }>;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Positional element references for use with `#[varName]` syntax.
|
|
56
|
+
* Resolves elements by child index path — no IDs assigned, no caching.
|
|
57
|
+
*
|
|
58
|
+
* - Array value: child index path (e.g., [0, 1] = target.children[0].children[1])
|
|
59
|
+
* - Object value: { path: [...], expect?: 'selector', fallback?: true }
|
|
60
|
+
* expect: validates via element.matches(), logs correction if wrong
|
|
61
|
+
* fallback: on mismatch, recovers via querySelector(expect)
|
|
62
|
+
*/
|
|
63
|
+
at?: Record<string, number[] | { path: number[]; expect?: string; fallback?: boolean }>;
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* Handler implementations scoped to this call.
|
|
56
67
|
* Key: the `do` name referenced in handler configs.
|
|
@@ -79,13 +90,16 @@ export interface AssignFromOptions extends IAssignGingerlyOptions {
|
|
|
79
90
|
* matched element (textContent, value, checked, dateTime, ish, etc.).
|
|
80
91
|
*
|
|
81
92
|
* @example
|
|
82
|
-
*
|
|
93
|
+
* infer: {
|
|
83
94
|
* byItemprop: ['user', 'name', 'email'], // or true for all source keys
|
|
84
95
|
* beVigilant: true, // watch for new matching elements (requires signal)
|
|
85
96
|
* }
|
|
86
97
|
*/
|
|
87
|
-
|
|
98
|
+
infer?: {
|
|
88
99
|
byItemprop?: string[] | true;
|
|
100
|
+
'|'?: string[] | true;
|
|
101
|
+
byName?: string[] | true | { props: string[] | true; outside: string };
|
|
102
|
+
'@'?: string[] | true | { props: string[] | true; outside: string };
|
|
89
103
|
/** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
|
|
90
104
|
beVigilant?: boolean;
|
|
91
105
|
};
|
|
@@ -150,13 +164,14 @@ export async function assignFromAsync(
|
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
153
|
-
if (idRefNormalKeys.length > 0 && options.withIds) {
|
|
167
|
+
if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
|
|
168
|
+
const ids = { ...options.withIds, ...options.at };
|
|
154
169
|
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
155
170
|
for (const key of idRefNormalKeys) {
|
|
156
171
|
const parsed = parseIdRef(key);
|
|
157
172
|
if (!parsed) continue;
|
|
158
173
|
|
|
159
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
174
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
160
175
|
if (!el) continue;
|
|
161
176
|
|
|
162
177
|
const value = expandedPattern[key];
|
|
@@ -190,7 +205,8 @@ export async function assignFromAsync(
|
|
|
190
205
|
await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
191
206
|
}
|
|
192
207
|
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
193
|
-
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
208
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
209
|
+
const ids = { ...options.withIds, ...options.at };
|
|
194
210
|
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
195
211
|
_processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
|
|
196
212
|
|
|
@@ -198,7 +214,7 @@ export async function assignFromAsync(
|
|
|
198
214
|
const parsed = parseIdRef(key);
|
|
199
215
|
if (!parsed) continue;
|
|
200
216
|
|
|
201
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
217
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
202
218
|
if (!el) continue;
|
|
203
219
|
|
|
204
220
|
// Build a synthetic key for processHandlerCommands:
|
|
@@ -216,17 +232,17 @@ export async function assignFromAsync(
|
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
// Process inferred assignments — dynamically imported only when option is present
|
|
219
|
-
if (options.
|
|
235
|
+
if (options.infer) {
|
|
220
236
|
const { processInferredAssignments } = await import('./inferredAssignments.js');
|
|
221
|
-
await processInferredAssignments(target, options.from, options.
|
|
237
|
+
await processInferredAssignments(target, options.from, options.infer);
|
|
222
238
|
|
|
223
239
|
// Set up MutationObserver for new matching elements if beVigilant
|
|
224
|
-
if (options.
|
|
240
|
+
if (options.infer.beVigilant) {
|
|
225
241
|
if (!options.signal) {
|
|
226
|
-
throw new Error('assignFrom:
|
|
242
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
227
243
|
}
|
|
228
244
|
const { setupVigilantObserver } = await import('./beVigilant.js');
|
|
229
|
-
setupVigilantObserver(target, options.from, options.
|
|
245
|
+
setupVigilantObserver(target, options.from, options.infer, options.signal);
|
|
230
246
|
}
|
|
231
247
|
}
|
|
232
248
|
|
package/assignGingerly.js
CHANGED
|
@@ -226,6 +226,21 @@ function parseDeleteCommand(key) {
|
|
|
226
226
|
}
|
|
227
227
|
return key.substring(0, key.length - 3); // Remove ' -=' suffix
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Helper function to check if a key represents a Y= merge command
|
|
231
|
+
*/
|
|
232
|
+
function isMergeCommand(key) {
|
|
233
|
+
return key.endsWith(' Y=');
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Helper function to parse a Y= merge command and extract the path
|
|
237
|
+
*/
|
|
238
|
+
function parseMergeCommand(key) {
|
|
239
|
+
if (!isMergeCommand(key)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return key.substring(0, key.length - 3); // Remove ' Y=' suffix
|
|
243
|
+
}
|
|
229
244
|
/**
|
|
230
245
|
* Helper function to parse a path string with ?. notation
|
|
231
246
|
* Always splits on '?.' delimiter, preserving dots that are part of values
|
|
@@ -769,6 +784,41 @@ export function assignGingerly(target, source, options, permissions) {
|
|
|
769
784
|
}
|
|
770
785
|
continue;
|
|
771
786
|
}
|
|
787
|
+
// Handle Y= merge commands (recursive assignGingerly into sub-object)
|
|
788
|
+
if (isMergeCommand(key)) {
|
|
789
|
+
const path = parseMergeCommand(key);
|
|
790
|
+
if (path) {
|
|
791
|
+
// Navigate to the target sub-object
|
|
792
|
+
let mergeTarget;
|
|
793
|
+
if (isNestedPath(path)) {
|
|
794
|
+
if (withMethodsSet) {
|
|
795
|
+
const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
|
|
796
|
+
mergeTarget = result.target[result.lastKey];
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
const pathParts = parsePath(path);
|
|
800
|
+
mergeTarget = target;
|
|
801
|
+
for (const part of pathParts) {
|
|
802
|
+
if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
|
|
803
|
+
mergeTarget = mergeTarget[part];
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
mergeTarget = undefined;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
mergeTarget = target[path];
|
|
814
|
+
}
|
|
815
|
+
// Recursively merge if target is a valid object
|
|
816
|
+
if (mergeTarget && typeof mergeTarget === 'object') {
|
|
817
|
+
assignGingerly(mergeTarget, value, options, permissions);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
772
822
|
if (isNestedPath(key)) {
|
|
773
823
|
const pathParts = parsePath(key);
|
|
774
824
|
// Check if path contains @each or @eachTime (forEach)
|
package/assignGingerly.ts
CHANGED
|
@@ -388,6 +388,23 @@ function parseDeleteCommand(key: string): string | null {
|
|
|
388
388
|
return key.substring(0, key.length - 3); // Remove ' -=' suffix
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Helper function to check if a key represents a Y= merge command
|
|
393
|
+
*/
|
|
394
|
+
function isMergeCommand(key: string): boolean {
|
|
395
|
+
return key.endsWith(' Y=');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Helper function to parse a Y= merge command and extract the path
|
|
400
|
+
*/
|
|
401
|
+
function parseMergeCommand(key: string): string | null {
|
|
402
|
+
if (!isMergeCommand(key)) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return key.substring(0, key.length - 3); // Remove ' Y=' suffix
|
|
406
|
+
}
|
|
407
|
+
|
|
391
408
|
/**
|
|
392
409
|
* Helper function to parse a path string with ?. notation
|
|
393
410
|
* Always splits on '?.' delimiter, preserving dots that are part of values
|
|
@@ -968,6 +985,40 @@ export function assignGingerly(
|
|
|
968
985
|
continue;
|
|
969
986
|
}
|
|
970
987
|
|
|
988
|
+
// Handle Y= merge commands (recursive assignGingerly into sub-object)
|
|
989
|
+
if (isMergeCommand(key)) {
|
|
990
|
+
const path = parseMergeCommand(key);
|
|
991
|
+
if (path) {
|
|
992
|
+
// Navigate to the target sub-object
|
|
993
|
+
let mergeTarget: any;
|
|
994
|
+
if (isNestedPath(path)) {
|
|
995
|
+
if (withMethodsSet) {
|
|
996
|
+
const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
|
|
997
|
+
mergeTarget = result.target[result.lastKey];
|
|
998
|
+
} else {
|
|
999
|
+
const pathParts = parsePath(path);
|
|
1000
|
+
mergeTarget = target;
|
|
1001
|
+
for (const part of pathParts) {
|
|
1002
|
+
if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
|
|
1003
|
+
mergeTarget = mergeTarget[part];
|
|
1004
|
+
} else {
|
|
1005
|
+
mergeTarget = undefined;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
mergeTarget = target[path];
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Recursively merge if target is a valid object
|
|
1015
|
+
if (mergeTarget && typeof mergeTarget === 'object') {
|
|
1016
|
+
assignGingerly(mergeTarget, value, options, permissions);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
971
1022
|
if (isNestedPath(key)) {
|
|
972
1023
|
const pathParts = parsePath(key);
|
|
973
1024
|
|
package/builtInEmoji.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtInEmoji.js — Predefined emoji aliases for built-in handlers.
|
|
3
|
+
*
|
|
4
|
+
* Import and spread into the `handlers` option for concise handler configs:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
8
|
+
*
|
|
9
|
+
* assignFrom(target, {
|
|
10
|
+
* '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
|
|
11
|
+
* }, { from: vm, handlers: builtInEmoji });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emoji → built-in handler name mapping.
|
|
16
|
+
*/
|
|
17
|
+
export const builtInEmoji = {
|
|
18
|
+
'📦': 'builtIns.lazyLoad',
|
|
19
|
+
'🎚️': 'builtIns.lazyLoadSwitch',
|
|
20
|
+
'🔗': 'builtIns.join',
|
|
21
|
+
'🏷️': 'builtIns.microDataJoin',
|
|
22
|
+
'📋': 'builtIns.manageTemplateList',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default builtInEmoji;
|
package/builtInEmoji.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
|
|
3
|
+
*
|
|
4
|
+
* Import and spread into the `handlers` option for concise handler configs:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
8
|
+
*
|
|
9
|
+
* assignFrom(target, {
|
|
10
|
+
* '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
|
|
11
|
+
* }, { from: vm, handlers: builtInEmoji });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emoji → built-in handler name mapping.
|
|
16
|
+
*
|
|
17
|
+
* | Emoji | Handler |
|
|
18
|
+
* |-------|---------|
|
|
19
|
+
* | 📦 | builtIns.lazyLoad |
|
|
20
|
+
* | 🎚️ | builtIns.lazyLoadSwitch |
|
|
21
|
+
* | 🔗 | builtIns.join |
|
|
22
|
+
* | 🏷️ | builtIns.microDataJoin |
|
|
23
|
+
* | 📋 | builtIns.manageTemplateList |
|
|
24
|
+
*/
|
|
25
|
+
export const builtInEmoji: Record<string, string> = {
|
|
26
|
+
'📦': 'builtIns.lazyLoad',
|
|
27
|
+
'🎚️': 'builtIns.lazyLoadSwitch',
|
|
28
|
+
'🔗': 'builtIns.join',
|
|
29
|
+
'🏷️': 'builtIns.microDataJoin',
|
|
30
|
+
'📋': 'builtIns.manageTemplateList',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default builtInEmoji;
|
package/handlers/lazyLoad.js
CHANGED
|
@@ -37,14 +37,17 @@ function getMarkerName(templateEl) {
|
|
|
37
37
|
*
|
|
38
38
|
* Exported so it can be subclassed for custom behavior.
|
|
39
39
|
*/
|
|
40
|
+
import { assignFrom } from '../assignFrom.js';
|
|
40
41
|
export class LazyLoadHandler {
|
|
41
42
|
config;
|
|
43
|
+
_options;
|
|
42
44
|
static #markerCounter = 0;
|
|
43
45
|
constructor(config) {
|
|
44
46
|
this.config = config;
|
|
45
47
|
}
|
|
46
|
-
async assign(lhsTarget, resolvedParams) {
|
|
47
|
-
|
|
48
|
+
async assign(lhsTarget, resolvedParams, options) {
|
|
49
|
+
this._options = options;
|
|
50
|
+
const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS, hideCss, markerName, toggleInert = false, toggleDisabled = false, placeholder } = resolvedParams;
|
|
48
51
|
if (!(lhsTarget instanceof Element)) {
|
|
49
52
|
throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
|
|
50
53
|
}
|
|
@@ -83,6 +86,16 @@ export class LazyLoadHandler {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
else {
|
|
89
|
+
// Remove placeholder content if specified
|
|
90
|
+
if (placeholder) {
|
|
91
|
+
const [phStart, phEnd] = findMarkers(lhsTarget, placeholder);
|
|
92
|
+
if (phStart && phEnd) {
|
|
93
|
+
const phNodes = getNodesBetweenMarkers(phStart, phEnd);
|
|
94
|
+
for (const node of phNodes) {
|
|
95
|
+
node.parentNode?.removeChild(node);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
86
99
|
if (method === 'after') {
|
|
87
100
|
[startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
|
|
88
101
|
} else {
|
|
@@ -164,6 +177,7 @@ export class LazyLoadHandler {
|
|
|
164
177
|
throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
|
|
165
178
|
}
|
|
166
179
|
const nodes = Array.from(content.childNodes);
|
|
180
|
+
this.applyAssign(nodes, resolvedParams);
|
|
167
181
|
endMarker.parentNode.insertBefore(content, endMarker);
|
|
168
182
|
return nodes;
|
|
169
183
|
}
|
|
@@ -179,6 +193,7 @@ export class LazyLoadHandler {
|
|
|
179
193
|
throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
|
|
180
194
|
}
|
|
181
195
|
const nodes = Array.from(content.childNodes);
|
|
196
|
+
this.applyAssign(nodes, resolvedParams);
|
|
182
197
|
endMarker.parentNode.insertBefore(content, endMarker);
|
|
183
198
|
await this.onCloneInserted(nodes, lhsTarget, resolvedParams);
|
|
184
199
|
if (resolvedParams.onInstantiated && typeof resolvedParams.onInstantiated === 'function') {
|
|
@@ -192,6 +207,22 @@ export class LazyLoadHandler {
|
|
|
192
207
|
}
|
|
193
208
|
return nodes;
|
|
194
209
|
}
|
|
210
|
+
applyAssign(nodes, resolvedParams) {
|
|
211
|
+
const assign = resolvedParams.assign;
|
|
212
|
+
if (!assign) return;
|
|
213
|
+
const elements = nodes.filter(n => n instanceof Element);
|
|
214
|
+
if (elements.length === 0) return;
|
|
215
|
+
const from = this._options?.from ?? {};
|
|
216
|
+
if (assign.configs && Array.isArray(assign.configs)) {
|
|
217
|
+
const len = Math.min(elements.length, assign.configs.length);
|
|
218
|
+
for (let j = 0; j < len; j++) {
|
|
219
|
+
const cfg = assign.configs[j];
|
|
220
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
|
|
221
|
+
}
|
|
222
|
+
} else if (assign.assignToFragment) {
|
|
223
|
+
assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
195
226
|
async onCloneInserted(nodes, lhsTarget, resolvedParams) {
|
|
196
227
|
// No-op by default. Subclasses override.
|
|
197
228
|
}
|
package/handlers/lazyLoad.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
|
23
23
|
import type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
|
|
24
24
|
import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
|
|
25
25
|
import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
|
|
26
|
+
import { assignFrom } from '../assignFrom.js';
|
|
26
27
|
|
|
27
28
|
export type { LazyLoadResolvedParams, LazyLoadInstantiatedContext };
|
|
28
29
|
|
|
@@ -46,13 +47,15 @@ function getMarkerName(templateEl: any): string {
|
|
|
46
47
|
*/
|
|
47
48
|
export class LazyLoadHandler implements AssignFromHandler {
|
|
48
49
|
config: any;
|
|
50
|
+
_options: any;
|
|
49
51
|
static #markerCounter = 0;
|
|
50
52
|
|
|
51
53
|
constructor(config: any) {
|
|
52
54
|
this.config = config;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams): Promise<void> {
|
|
57
|
+
async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams, options?: any): Promise<void> {
|
|
58
|
+
this._options = options; // Store for applyAssign access
|
|
56
59
|
const {
|
|
57
60
|
if: condition,
|
|
58
61
|
instantiate,
|
|
@@ -64,6 +67,7 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
64
67
|
markerName,
|
|
65
68
|
toggleInert = false,
|
|
66
69
|
toggleDisabled = false,
|
|
70
|
+
placeholder,
|
|
67
71
|
} = resolvedParams;
|
|
68
72
|
|
|
69
73
|
if (!(lhsTarget instanceof Element)) {
|
|
@@ -112,6 +116,16 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
112
116
|
}
|
|
113
117
|
} else {
|
|
114
118
|
// No markers — first time. Create markers and clone template.
|
|
119
|
+
// Remove placeholder content if specified
|
|
120
|
+
if (placeholder) {
|
|
121
|
+
const [phStart, phEnd] = findMarkers(lhsTarget, placeholder);
|
|
122
|
+
if (phStart && phEnd) {
|
|
123
|
+
const phNodes = getNodesBetweenMarkers(phStart, phEnd);
|
|
124
|
+
for (const node of phNodes) {
|
|
125
|
+
node.parentNode?.removeChild(node);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
115
129
|
if (method === 'after') {
|
|
116
130
|
[startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
|
|
117
131
|
} else {
|
|
@@ -229,6 +243,10 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
const nodes = Array.from(content.childNodes);
|
|
246
|
+
|
|
247
|
+
// Apply assignments to cloned content before insertion
|
|
248
|
+
this.applyAssign(nodes, resolvedParams);
|
|
249
|
+
|
|
232
250
|
endMarker.parentNode!.insertBefore(content, endMarker);
|
|
233
251
|
return nodes;
|
|
234
252
|
}
|
|
@@ -259,6 +277,9 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
259
277
|
// Capture nodes before insertion (childNodes empties after insertBefore)
|
|
260
278
|
const nodes = Array.from(content.childNodes);
|
|
261
279
|
|
|
280
|
+
// Apply assignments to cloned content before insertion
|
|
281
|
+
this.applyAssign(nodes, resolvedParams);
|
|
282
|
+
|
|
262
283
|
// Insert before endMarker
|
|
263
284
|
endMarker.parentNode!.insertBefore(content, endMarker);
|
|
264
285
|
|
|
@@ -279,6 +300,30 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
279
300
|
return nodes;
|
|
280
301
|
}
|
|
281
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Apply assignFrom to cloned nodes based on the `assign` config.
|
|
305
|
+
* Supports single-element and multi-element (configs array) templates.
|
|
306
|
+
*/
|
|
307
|
+
protected applyAssign(nodes: Node[], resolvedParams: LazyLoadResolvedParams): void {
|
|
308
|
+
const assign = (resolvedParams as any).assign;
|
|
309
|
+
if (!assign) return;
|
|
310
|
+
|
|
311
|
+
const elements = nodes.filter(n => n instanceof Element) as Element[];
|
|
312
|
+
if (elements.length === 0) return;
|
|
313
|
+
|
|
314
|
+
const from = this._options?.from ?? {};
|
|
315
|
+
|
|
316
|
+
if (assign.configs && Array.isArray(assign.configs)) {
|
|
317
|
+
const len = Math.min(elements.length, assign.configs.length);
|
|
318
|
+
for (let j = 0; j < len; j++) {
|
|
319
|
+
const cfg = assign.configs[j];
|
|
320
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
|
|
321
|
+
}
|
|
322
|
+
} else if (assign.assignToFragment) {
|
|
323
|
+
assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
282
327
|
/**
|
|
283
328
|
* Hook called after template content is cloned and inserted.
|
|
284
329
|
* Override in subclasses for custom post-clone logic.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* },
|
|
19
19
|
* fromEachItem: {
|
|
20
20
|
* assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
21
|
-
* withOptions: { withMethods: ['querySelector'],
|
|
21
|
+
* withOptions: { withMethods: ['querySelector'], infer: true },
|
|
22
22
|
* resolve: { key: '?.rank' }
|
|
23
23
|
* }
|
|
24
24
|
* }
|
|
@@ -47,6 +47,7 @@ export class ManageTemplateListHandler {
|
|
|
47
47
|
return; // Nothing to iterate
|
|
48
48
|
}
|
|
49
49
|
const fromEachItem = this.config.fromEachItem;
|
|
50
|
+
const configs = fromEachItem?.configs; // Array form for multi-element templates
|
|
50
51
|
const assignToFragment = fromEachItem?.assignToFragment ?? {};
|
|
51
52
|
const withOptions = fromEachItem?.withOptions ?? {};
|
|
52
53
|
const perItemResolve = fromEachItem?.resolve ?? {};
|
|
@@ -55,10 +56,10 @@ export class ManageTemplateListHandler {
|
|
|
55
56
|
const fromSource = this.config.fromSource;
|
|
56
57
|
const sourceAssignToFragment = fromSource?.assignToFragment;
|
|
57
58
|
const sourceWithOptions = fromSource?.withOptions ?? {};
|
|
58
|
-
// Detect fast path: no assignToFragment patterns, just
|
|
59
|
-
const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
|
|
60
|
-
const inferredConfig = withOptions.
|
|
61
|
-
const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
59
|
+
// Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
|
|
60
|
+
const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
|
|
61
|
+
const inferredConfig = !configs && withOptions.infer;
|
|
62
|
+
const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
62
63
|
const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
|
|
63
64
|
// Find or create markers
|
|
64
65
|
let [startMarker, endMarker] = findMarkers(lhsTarget, name);
|
|
@@ -112,19 +113,30 @@ export class ManageTemplateListHandler {
|
|
|
112
113
|
if (state.keyToNodes.has(key) && oldKeys.has(key)) {
|
|
113
114
|
// Existing item — update in place
|
|
114
115
|
const existingNodes = state.keyToNodes.get(key);
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
117
|
+
if (shouldYield)
|
|
118
|
+
await new Promise(r => setTimeout(r, 0));
|
|
119
|
+
if (configs) {
|
|
120
|
+
// Multi-element: zip configs with element nodes
|
|
121
|
+
const elements = existingNodes.filter(n => n instanceof Element);
|
|
122
|
+
const len = Math.min(elements.length, configs.length);
|
|
123
|
+
for (let j = 0; j < len; j++) {
|
|
124
|
+
const cfg = configs[j];
|
|
125
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
122
126
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const rootEl = existingNodes.find(n => n instanceof Element);
|
|
130
|
+
if (rootEl) {
|
|
131
|
+
if (processInferred) {
|
|
132
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
136
|
+
}
|
|
137
|
+
if (sourceAssignToFragment && options?.from) {
|
|
138
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
139
|
+
}
|
|
128
140
|
}
|
|
129
141
|
}
|
|
130
142
|
newKeyToNodes.set(key, existingNodes);
|
|
@@ -147,21 +159,32 @@ export class ManageTemplateListHandler {
|
|
|
147
159
|
}
|
|
148
160
|
const clonedNodes = Array.from(content.childNodes);
|
|
149
161
|
// Apply per-item assignments to the cloned fragment
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
163
|
+
if (shouldYield)
|
|
164
|
+
await new Promise(r => setTimeout(r, 0));
|
|
165
|
+
if (configs) {
|
|
166
|
+
// Multi-element: zip configs with element nodes
|
|
167
|
+
const elements = clonedNodes.filter(n => n instanceof Element);
|
|
168
|
+
const len = Math.min(elements.length, configs.length);
|
|
169
|
+
for (let j = 0; j < len; j++) {
|
|
170
|
+
const cfg = configs[j];
|
|
171
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
159
172
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const rootEl = clonedNodes.find(n => n instanceof Element);
|
|
176
|
+
if (rootEl) {
|
|
177
|
+
const tempContainer = document.createDocumentFragment();
|
|
178
|
+
tempContainer.appendChild(content);
|
|
179
|
+
if (processInferred) {
|
|
180
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
184
|
+
}
|
|
185
|
+
if (sourceAssignToFragment && options?.from) {
|
|
186
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
187
|
+
}
|
|
165
188
|
}
|
|
166
189
|
}
|
|
167
190
|
for (const node of clonedNodes) {
|