assign-gingerly 0.0.58 → 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 +511 -142
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +306 -133
- package/assignFrom.ts +318 -232
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +120 -0
- package/assignFromAsync.ts +256 -0
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/getValues.js +223 -0
- package/getValues.ts +255 -0
- package/handlers/join.ts +1 -1
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +47 -2
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.js +226 -0
- package/handlers/manageTemplateList.ts +263 -0
- package/handlers/microDataJoin.ts +1 -1
- package/index.js +1 -0
- package/index.ts +2 -0
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +35 -5
- package/inferredAssignments.ts +58 -8
- package/markerUtils.js +136 -127
- package/package.json +30 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +36 -4
- package/processHandlerCommands.ts +38 -8
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/resolveValues.js +41 -125
- package/resolveValues.ts +131 -255
- package/types/assign-gingerly/types.d.ts +61 -0
- package/waitForSettled.js +57 -0
- package/waitForSettled.ts +65 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.manageTemplateList handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Clones a template once per item in an iterable, distributing each item's
|
|
5
|
+
* properties into its clone via assignFrom. Manages the list over time —
|
|
6
|
+
* reconciling by key to add, remove, and update-in-place.
|
|
7
|
+
*
|
|
8
|
+
* This handler is auto-loaded by processHandlerCommands when
|
|
9
|
+
* `do: 'builtIns.manageTemplateList'` is encountered.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* assignFrom(document.body, {
|
|
13
|
+
* '?.querySelector?.tbody =>': {
|
|
14
|
+
* do: 'builtIns.manageTemplateList',
|
|
15
|
+
* resolve: {
|
|
16
|
+
* forEach: '?.rankings',
|
|
17
|
+
* instantiate: 'globalThis://country-ranking',
|
|
18
|
+
* },
|
|
19
|
+
* fromEachItem: {
|
|
20
|
+
* assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
21
|
+
* withOptions: { withMethods: ['querySelector'], infer: true },
|
|
22
|
+
* resolve: { key: '?.rank' }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
26
|
+
*/
|
|
27
|
+
import { findMarkers, createMarkers } from '../markerUtils.js';
|
|
28
|
+
import { resolveValue } from '../resolveValues.js';
|
|
29
|
+
import { assignFrom } from '../assignFrom.js';
|
|
30
|
+
import { processInferredAssignments } from '../inferredAssignments.js';
|
|
31
|
+
const listStateMap = new WeakMap();
|
|
32
|
+
/**
|
|
33
|
+
* ManageTemplateListHandler — clones a template per iterable item with keyed reconciliation.
|
|
34
|
+
*/
|
|
35
|
+
export class ManageTemplateListHandler {
|
|
36
|
+
config;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
}
|
|
40
|
+
async assign(lhsTarget, resolvedParams, options) {
|
|
41
|
+
//return;
|
|
42
|
+
const { forEach: items, instantiate, method = 'appendChild', forget = false, markerName, yieldEvery, } = resolvedParams;
|
|
43
|
+
if (!(lhsTarget instanceof Element)) {
|
|
44
|
+
throw new Error('builtIns.manageTemplateList: lhsTarget must be a DOM Element');
|
|
45
|
+
}
|
|
46
|
+
if (!items || typeof items[Symbol.iterator] !== 'function') {
|
|
47
|
+
return; // Nothing to iterate
|
|
48
|
+
}
|
|
49
|
+
const fromEachItem = this.config.fromEachItem;
|
|
50
|
+
const configs = fromEachItem?.configs; // Array form for multi-element templates
|
|
51
|
+
const assignToFragment = fromEachItem?.assignToFragment ?? {};
|
|
52
|
+
const withOptions = fromEachItem?.withOptions ?? {};
|
|
53
|
+
const perItemResolve = fromEachItem?.resolve ?? {};
|
|
54
|
+
const keyPath = perItemResolve.key; // e.g., '?.rank'
|
|
55
|
+
// fromSource config — assigns from the outer `from` (parent VM) to each clone
|
|
56
|
+
const fromSource = this.config.fromSource;
|
|
57
|
+
const sourceAssignToFragment = fromSource?.assignToFragment;
|
|
58
|
+
const sourceWithOptions = fromSource?.withOptions ?? {};
|
|
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;
|
|
63
|
+
const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
|
|
64
|
+
// Find or create markers
|
|
65
|
+
let [startMarker, endMarker] = findMarkers(lhsTarget, name);
|
|
66
|
+
if (!startMarker || !endMarker) {
|
|
67
|
+
[startMarker, endMarker] = createMarkers(lhsTarget, name, method);
|
|
68
|
+
}
|
|
69
|
+
// Get or create list state
|
|
70
|
+
let state = listStateMap.get(startMarker);
|
|
71
|
+
if (!state) {
|
|
72
|
+
state = { keyToNodes: new Map(), keyOrder: [] };
|
|
73
|
+
listStateMap.set(startMarker, state);
|
|
74
|
+
}
|
|
75
|
+
// Convert iterable to array
|
|
76
|
+
const itemsArray = Array.from(items);
|
|
77
|
+
// Fast path: use processInferredAssignments directly when possible
|
|
78
|
+
const processInferred = useFastPath ? processInferredAssignments : null;
|
|
79
|
+
const newKeys = itemsArray.map((item, index) => {
|
|
80
|
+
if (keyPath) {
|
|
81
|
+
return resolveValue(keyPath, item);
|
|
82
|
+
}
|
|
83
|
+
return index; // Positional fallback when no key specified
|
|
84
|
+
});
|
|
85
|
+
// Determine what changed
|
|
86
|
+
const oldKeys = new Set(state.keyOrder);
|
|
87
|
+
const newKeySet = new Set(newKeys);
|
|
88
|
+
// Keys to remove
|
|
89
|
+
for (const oldKey of state.keyOrder) {
|
|
90
|
+
if (!newKeySet.has(oldKey)) {
|
|
91
|
+
const nodes = state.keyToNodes.get(oldKey);
|
|
92
|
+
if (nodes) {
|
|
93
|
+
if (forget) {
|
|
94
|
+
for (const node of nodes)
|
|
95
|
+
node.parentNode?.removeChild(node);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
for (const node of nodes) {
|
|
99
|
+
if (node instanceof Element)
|
|
100
|
+
node.setAttribute('hidden', '');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
state.keyToNodes.delete(oldKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Process items — clone new ones, update existing
|
|
108
|
+
const fragment = document.createDocumentFragment();
|
|
109
|
+
const newKeyToNodes = new Map();
|
|
110
|
+
for (let i = 0; i < itemsArray.length; i++) {
|
|
111
|
+
const item = itemsArray[i];
|
|
112
|
+
const key = newKeys[i];
|
|
113
|
+
if (state.keyToNodes.has(key) && oldKeys.has(key)) {
|
|
114
|
+
// Existing item — update in place
|
|
115
|
+
const existingNodes = state.keyToNodes.get(key);
|
|
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 });
|
|
126
|
+
}
|
|
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
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
newKeyToNodes.set(key, existingNodes);
|
|
143
|
+
for (const node of existingNodes) {
|
|
144
|
+
if (node instanceof Element)
|
|
145
|
+
node.removeAttribute('hidden');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// New item — clone template
|
|
150
|
+
let content;
|
|
151
|
+
if (instantiate instanceof HTMLTemplateElement) {
|
|
152
|
+
content = instantiate.content.cloneNode(true);
|
|
153
|
+
}
|
|
154
|
+
else if (instantiate instanceof DocumentFragment) {
|
|
155
|
+
content = instantiate.cloneNode(true);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
throw new Error('builtIns.manageTemplateList: instantiate must be an HTMLTemplateElement or DocumentFragment');
|
|
159
|
+
}
|
|
160
|
+
const clonedNodes = Array.from(content.childNodes);
|
|
161
|
+
// Apply per-item assignments to the cloned fragment
|
|
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 });
|
|
172
|
+
}
|
|
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
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const node of clonedNodes) {
|
|
191
|
+
fragment.appendChild(node);
|
|
192
|
+
}
|
|
193
|
+
newKeyToNodes.set(key, clonedNodes);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Wait for async rendering in the fragment to settle before committing
|
|
197
|
+
if (fragment.childNodes.length > 0) {
|
|
198
|
+
const waitOpt = resolvedParams.waitForSettled;
|
|
199
|
+
if (waitOpt) {
|
|
200
|
+
const { waitForSettled } = await import('../waitForSettled.js');
|
|
201
|
+
const idleMs = typeof waitOpt === 'object' ? waitOpt.idleMs : 100;
|
|
202
|
+
const timeout = typeof waitOpt === 'object' ? waitOpt.timeout : undefined;
|
|
203
|
+
try {
|
|
204
|
+
await waitForSettled(fragment, idleMs, timeout);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
console.warn('builtIns.manageTemplateList:', e.message, '— inserting fragment anyway');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Insert new fragment before end marker
|
|
211
|
+
endMarker.parentNode.insertBefore(fragment, endMarker);
|
|
212
|
+
}
|
|
213
|
+
// Update state
|
|
214
|
+
state.keyToNodes = newKeyToNodes;
|
|
215
|
+
state.keyOrder = newKeys;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get marker name from a template element.
|
|
220
|
+
*/
|
|
221
|
+
function getMarkerName(templateEl) {
|
|
222
|
+
if (templateEl instanceof HTMLTemplateElement) {
|
|
223
|
+
return templateEl.id || undefined;
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.manageTemplateList handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Clones a template once per item in an iterable, distributing each item's
|
|
5
|
+
* properties into its clone via assignFrom. Manages the list over time —
|
|
6
|
+
* reconciling by key to add, remove, and update-in-place.
|
|
7
|
+
*
|
|
8
|
+
* This handler is auto-loaded by processHandlerCommands when
|
|
9
|
+
* `do: 'builtIns.manageTemplateList'` is encountered.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* assignFrom(document.body, {
|
|
13
|
+
* '?.querySelector?.tbody =>': {
|
|
14
|
+
* do: 'builtIns.manageTemplateList',
|
|
15
|
+
* resolve: {
|
|
16
|
+
* forEach: '?.rankings',
|
|
17
|
+
* instantiate: 'globalThis://country-ranking',
|
|
18
|
+
* },
|
|
19
|
+
* fromEachItem: {
|
|
20
|
+
* assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
21
|
+
* withOptions: { withMethods: ['querySelector'], infer: true },
|
|
22
|
+
* resolve: { key: '?.rank' }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
29
|
+
import type { ManageTemplateListResolvedParams } from '../types/assign-gingerly/types.js';
|
|
30
|
+
import { findMarkers, createMarkers, getNodesBetweenMarkers, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
|
|
31
|
+
import { resolveValue } from '../resolveValues.js';
|
|
32
|
+
import { assignFrom } from '../assignFrom.js';
|
|
33
|
+
import { processInferredAssignments } from '../inferredAssignments.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* State stored per list instance (keyed by start marker).
|
|
37
|
+
* Tracks the mapping of keys to their cloned DOM nodes.
|
|
38
|
+
*/
|
|
39
|
+
interface ListState {
|
|
40
|
+
/** Map of key value → array of DOM nodes for that item's clone */
|
|
41
|
+
keyToNodes: Map<any, Node[]>;
|
|
42
|
+
/** Ordered array of keys matching current DOM order */
|
|
43
|
+
keyOrder: any[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const listStateMap = new WeakMap<Node, ListState>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ManageTemplateListHandler — clones a template per iterable item with keyed reconciliation.
|
|
50
|
+
*/
|
|
51
|
+
export class ManageTemplateListHandler implements AssignFromHandler {
|
|
52
|
+
config: any;
|
|
53
|
+
|
|
54
|
+
constructor(config: any) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async assign(lhsTarget: any, resolvedParams: ManageTemplateListResolvedParams, options?: any): Promise<void> {
|
|
59
|
+
//return;
|
|
60
|
+
const {
|
|
61
|
+
forEach: items,
|
|
62
|
+
instantiate,
|
|
63
|
+
method = 'appendChild',
|
|
64
|
+
forget = false,
|
|
65
|
+
markerName,
|
|
66
|
+
yieldEvery,
|
|
67
|
+
} = resolvedParams;
|
|
68
|
+
|
|
69
|
+
if (!(lhsTarget instanceof Element)) {
|
|
70
|
+
throw new Error('builtIns.manageTemplateList: lhsTarget must be a DOM Element');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!items || typeof items[Symbol.iterator] !== 'function') {
|
|
74
|
+
return; // Nothing to iterate
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fromEachItem = this.config.fromEachItem;
|
|
78
|
+
const configs = fromEachItem?.configs; // Array form for multi-element templates
|
|
79
|
+
const assignToFragment = fromEachItem?.assignToFragment ?? {};
|
|
80
|
+
const withOptions = fromEachItem?.withOptions ?? {};
|
|
81
|
+
const perItemResolve = fromEachItem?.resolve ?? {};
|
|
82
|
+
const keyPath = perItemResolve.key; // e.g., '?.rank'
|
|
83
|
+
|
|
84
|
+
// fromSource config — assigns from the outer `from` (parent VM) to each clone
|
|
85
|
+
const fromSource = this.config.fromSource;
|
|
86
|
+
const sourceAssignToFragment = fromSource?.assignToFragment;
|
|
87
|
+
const sourceWithOptions = fromSource?.withOptions ?? {};
|
|
88
|
+
|
|
89
|
+
// Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
|
|
90
|
+
const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
|
|
91
|
+
const inferredConfig = !configs && withOptions.infer;
|
|
92
|
+
const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
93
|
+
|
|
94
|
+
const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
|
|
95
|
+
|
|
96
|
+
// Find or create markers
|
|
97
|
+
let [startMarker, endMarker] = findMarkers(lhsTarget, name);
|
|
98
|
+
if (!startMarker || !endMarker) {
|
|
99
|
+
[startMarker, endMarker] = createMarkers(lhsTarget, name, method);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get or create list state
|
|
103
|
+
let state = listStateMap.get(startMarker);
|
|
104
|
+
if (!state) {
|
|
105
|
+
state = { keyToNodes: new Map(), keyOrder: [] };
|
|
106
|
+
listStateMap.set(startMarker, state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Convert iterable to array
|
|
110
|
+
const itemsArray = Array.from(items);
|
|
111
|
+
|
|
112
|
+
// Fast path: use processInferredAssignments directly when possible
|
|
113
|
+
const processInferred = useFastPath ? processInferredAssignments : null;
|
|
114
|
+
const newKeys: any[] = itemsArray.map((item, index) => {
|
|
115
|
+
if (keyPath) {
|
|
116
|
+
return resolveValue(keyPath, item);
|
|
117
|
+
}
|
|
118
|
+
return index; // Positional fallback when no key specified
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Determine what changed
|
|
122
|
+
const oldKeys = new Set(state.keyOrder);
|
|
123
|
+
const newKeySet = new Set(newKeys);
|
|
124
|
+
|
|
125
|
+
// Keys to remove
|
|
126
|
+
for (const oldKey of state.keyOrder) {
|
|
127
|
+
if (!newKeySet.has(oldKey)) {
|
|
128
|
+
const nodes = state.keyToNodes.get(oldKey);
|
|
129
|
+
if (nodes) {
|
|
130
|
+
if (forget) {
|
|
131
|
+
for (const node of nodes) node.parentNode?.removeChild(node);
|
|
132
|
+
} else {
|
|
133
|
+
for (const node of nodes) {
|
|
134
|
+
if (node instanceof Element) node.setAttribute('hidden', '');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
state.keyToNodes.delete(oldKey);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Process items — clone new ones, update existing
|
|
143
|
+
const fragment = document.createDocumentFragment();
|
|
144
|
+
const newKeyToNodes = new Map<any, Node[]>();
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < itemsArray.length; i++) {
|
|
147
|
+
const item = itemsArray[i];
|
|
148
|
+
const key = newKeys[i];
|
|
149
|
+
|
|
150
|
+
if (state.keyToNodes.has(key) && oldKeys.has(key)) {
|
|
151
|
+
// Existing item — update in place
|
|
152
|
+
const existingNodes = state.keyToNodes.get(key)!;
|
|
153
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
154
|
+
if (shouldYield) await new Promise(r => setTimeout(r, 0));
|
|
155
|
+
|
|
156
|
+
if (configs) {
|
|
157
|
+
// Multi-element: zip configs with element nodes
|
|
158
|
+
const elements = existingNodes.filter(n => n instanceof Element) as Element[];
|
|
159
|
+
const len = Math.min(elements.length, configs.length);
|
|
160
|
+
for (let j = 0; j < len; j++) {
|
|
161
|
+
const cfg = configs[j];
|
|
162
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
|
|
166
|
+
if (rootEl) {
|
|
167
|
+
if (processInferred) {
|
|
168
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
169
|
+
} else {
|
|
170
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
171
|
+
}
|
|
172
|
+
if (sourceAssignToFragment && options?.from) {
|
|
173
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
newKeyToNodes.set(key, existingNodes);
|
|
178
|
+
for (const node of existingNodes) {
|
|
179
|
+
if (node instanceof Element) node.removeAttribute('hidden');
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// New item — clone template
|
|
183
|
+
let content: DocumentFragment;
|
|
184
|
+
if (instantiate instanceof HTMLTemplateElement) {
|
|
185
|
+
content = instantiate.content.cloneNode(true) as DocumentFragment;
|
|
186
|
+
} else if (instantiate instanceof DocumentFragment) {
|
|
187
|
+
content = instantiate.cloneNode(true) as DocumentFragment;
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error('builtIns.manageTemplateList: instantiate must be an HTMLTemplateElement or DocumentFragment');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const clonedNodes = Array.from(content.childNodes);
|
|
193
|
+
|
|
194
|
+
// Apply per-item assignments to the cloned fragment
|
|
195
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
196
|
+
if (shouldYield) await new Promise(r => setTimeout(r, 0));
|
|
197
|
+
|
|
198
|
+
if (configs) {
|
|
199
|
+
// Multi-element: zip configs with element nodes
|
|
200
|
+
const elements = clonedNodes.filter(n => n instanceof Element) as Element[];
|
|
201
|
+
const len = Math.min(elements.length, configs.length);
|
|
202
|
+
for (let j = 0; j < len; j++) {
|
|
203
|
+
const cfg = configs[j];
|
|
204
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
|
|
208
|
+
if (rootEl) {
|
|
209
|
+
const tempContainer = document.createDocumentFragment();
|
|
210
|
+
tempContainer.appendChild(content);
|
|
211
|
+
|
|
212
|
+
if (processInferred) {
|
|
213
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
214
|
+
} else {
|
|
215
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sourceAssignToFragment && options?.from) {
|
|
219
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const node of clonedNodes) {
|
|
225
|
+
fragment.appendChild(node);
|
|
226
|
+
}
|
|
227
|
+
newKeyToNodes.set(key, clonedNodes);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Wait for async rendering in the fragment to settle before committing
|
|
232
|
+
if (fragment.childNodes.length > 0) {
|
|
233
|
+
const waitOpt = resolvedParams.waitForSettled;
|
|
234
|
+
if (waitOpt) {
|
|
235
|
+
const { waitForSettled } = await import('../waitForSettled.js');
|
|
236
|
+
const idleMs = typeof waitOpt === 'object' ? waitOpt.idleMs : 100;
|
|
237
|
+
const timeout = typeof waitOpt === 'object' ? waitOpt.timeout : undefined;
|
|
238
|
+
try {
|
|
239
|
+
await waitForSettled(fragment, idleMs, timeout);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.warn('builtIns.manageTemplateList:', (e as Error).message, '— inserting fragment anyway');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Insert new fragment before end marker
|
|
246
|
+
endMarker.parentNode!.insertBefore(fragment, endMarker);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Update state
|
|
250
|
+
state.keyToNodes = newKeyToNodes;
|
|
251
|
+
state.keyOrder = newKeys;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get marker name from a template element.
|
|
257
|
+
*/
|
|
258
|
+
function getMarkerName(templateEl: any): string | undefined {
|
|
259
|
+
if (templateEl instanceof HTMLTemplateElement) {
|
|
260
|
+
return templateEl.id || undefined;
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* }, { from: vm, withMethods: ['querySelector'] });
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import type { AssignFromHandler } from '../
|
|
36
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
37
37
|
|
|
38
38
|
const MARKER_START_PREFIX = '?start name="';
|
|
39
39
|
const MARKER_END = '?end';
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { buildCSSQuery } from './buildCSSQuery.js';
|
|
|
8
8
|
export { resolveTemplate } from './resolveTemplate.js';
|
|
9
9
|
export { getHost } from './getHost.js';
|
|
10
10
|
export { resolveValues, resolveValue } from './resolveValues.js';
|
|
11
|
+
export { assignFromAsync } from './assignFromAsync.js';
|
|
11
12
|
export { assignFrom } from './assignFrom.js';
|
|
12
13
|
export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
|
|
13
14
|
export { installForwarding } from './installForwarding.js';
|
package/index.ts
CHANGED
|
@@ -8,9 +8,11 @@ export {buildCSSQuery} from './buildCSSQuery.js';
|
|
|
8
8
|
export {resolveTemplate} from './resolveTemplate.js';
|
|
9
9
|
export {getHost} from './getHost.js';
|
|
10
10
|
export {resolveValues, resolveValue} from './resolveValues.js';
|
|
11
|
+
export {assignFromAsync} from './assignFromAsync.js';
|
|
11
12
|
export {assignFrom} from './assignFrom.js';
|
|
12
13
|
export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
|
|
13
14
|
export {installForwarding} from './installForwarding.js';
|
|
14
15
|
export {defineWithFeatures} from './defineWithFeatures.js';
|
|
15
16
|
export {resolveAndAssignFeatures} from './resolveAndAssignFeatures.js';
|
|
17
|
+
export {} from './handlers/manageTemplateList.js';
|
|
16
18
|
import './object-extension.js';
|
package/inferencer/inferencer.js
CHANGED
|
@@ -81,38 +81,26 @@ export class Infer {
|
|
|
81
81
|
return inferValueProperty(this.enhancedElement);
|
|
82
82
|
}
|
|
83
83
|
['|'](itempropAttr, scopeBoundary = '[itemscope]') {
|
|
84
|
-
|
|
85
|
-
return Array.from(candidates)
|
|
86
|
-
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
87
|
-
.map(x => new Infer(x, itempropAttr));
|
|
84
|
+
return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
|
|
88
85
|
}
|
|
89
86
|
['@'](nameAttr, scopeBoundary) {
|
|
90
|
-
|
|
91
|
-
const filtered = scopeBoundary
|
|
92
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
93
|
-
: Array.from(candidates);
|
|
94
|
-
return filtered.map(x => new Infer(x, nameAttr));
|
|
87
|
+
return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
|
|
95
88
|
}
|
|
96
89
|
['%'](partAttr, scopeBoundary) {
|
|
97
|
-
|
|
98
|
-
const filtered = scopeBoundary
|
|
99
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
100
|
-
: Array.from(candidates);
|
|
101
|
-
return filtered.map(x => new Infer(x, partAttr));
|
|
90
|
+
return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
|
|
102
91
|
}
|
|
103
92
|
['#'](id, scopeBoundary) {
|
|
104
|
-
|
|
105
|
-
const filtered = scopeBoundary
|
|
106
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
107
|
-
: Array.from(candidates);
|
|
108
|
-
return filtered.map(x => new Infer(x, id));
|
|
93
|
+
return this.#queryScoped(`#${id}`, id, scopeBoundary);
|
|
109
94
|
}
|
|
110
95
|
['.'](className, scopeBoundary) {
|
|
111
|
-
|
|
96
|
+
return this.#queryScoped(`.${className}`, className, scopeBoundary);
|
|
97
|
+
}
|
|
98
|
+
#queryScoped(selector, propName, scopeBoundary) {
|
|
99
|
+
const candidates = this.enhancedElement.querySelectorAll(selector);
|
|
112
100
|
const filtered = scopeBoundary
|
|
113
101
|
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
114
102
|
: Array.from(candidates);
|
|
115
|
-
return filtered.map(x => new Infer(x,
|
|
103
|
+
return filtered.map(x => new Infer(x, propName));
|
|
116
104
|
}
|
|
117
105
|
setDisplay(vm) {
|
|
118
106
|
const val = this.#propName ? vm[this.#propName] : inferBindingProperty(this.enhancedElement);
|
package/inferencer/inferencer.ts
CHANGED
|
@@ -103,42 +103,31 @@ export class Infer<TValue = any, TDisplay = any> {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
['|'](itempropAttr: string, scopeBoundary: string = '[itemscope]'){
|
|
106
|
-
|
|
107
|
-
return Array.from(candidates)
|
|
108
|
-
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
109
|
-
.map(x => new Infer(x, itempropAttr));
|
|
106
|
+
return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
|
|
110
107
|
}
|
|
111
108
|
|
|
112
109
|
['@'](nameAttr: string, scopeBoundary?: string){
|
|
113
|
-
|
|
114
|
-
const filtered = scopeBoundary
|
|
115
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
116
|
-
: Array.from(candidates);
|
|
117
|
-
return filtered.map(x => new Infer(x, nameAttr));
|
|
110
|
+
return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
['%'](partAttr: string, scopeBoundary?: string){
|
|
121
|
-
|
|
122
|
-
const filtered = scopeBoundary
|
|
123
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
124
|
-
: Array.from(candidates);
|
|
125
|
-
return filtered.map(x => new Infer(x, partAttr));
|
|
114
|
+
return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
|
|
126
115
|
}
|
|
127
116
|
|
|
128
117
|
['#'](id: string, scopeBoundary?: string){
|
|
129
|
-
|
|
130
|
-
const filtered = scopeBoundary
|
|
131
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
132
|
-
: Array.from(candidates);
|
|
133
|
-
return filtered.map(x => new Infer(x, id));
|
|
118
|
+
return this.#queryScoped(`#${id}`, id, scopeBoundary);
|
|
134
119
|
}
|
|
135
120
|
|
|
136
121
|
['.'](className: string, scopeBoundary?: string){
|
|
137
|
-
|
|
122
|
+
return this.#queryScoped(`.${className}`, className, scopeBoundary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#queryScoped(selector: string, propName: string, scopeBoundary?: string): Infer[] {
|
|
126
|
+
const candidates = this.enhancedElement.querySelectorAll(selector);
|
|
138
127
|
const filtered = scopeBoundary
|
|
139
128
|
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
140
129
|
: Array.from(candidates);
|
|
141
|
-
return filtered.map(x => new Infer(x,
|
|
130
|
+
return filtered.map(x => new Infer(x, propName));
|
|
142
131
|
}
|
|
143
132
|
|
|
144
133
|
setDisplay(vm: any){
|