@plentico/pattr 0.0.4
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/LICENSE +21 -0
- package/README.md +414 -0
- package/package.json +58 -0
- package/pattr.js +997 -0
- package/pattr.min.js +7 -0
package/pattr.js
ADDED
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Pattr v1.0.0
|
|
3
|
+
* A lightweight reactive framework
|
|
4
|
+
* https://github.com/plentico/pattr
|
|
5
|
+
* MIT License
|
|
6
|
+
*/
|
|
7
|
+
window.Pattr = {
|
|
8
|
+
_templateScopeCounter: 0,
|
|
9
|
+
|
|
10
|
+
directives: {
|
|
11
|
+
'p-text': (el, value, modifiers = {}) => {
|
|
12
|
+
let text = String(value);
|
|
13
|
+
|
|
14
|
+
// Apply trim modifier
|
|
15
|
+
if (modifiers.trim && modifiers.trim.length > 0) {
|
|
16
|
+
const maxLength = parseInt(modifiers.trim[0]) || 100;
|
|
17
|
+
if (text.length > maxLength) {
|
|
18
|
+
text = text.substring(0, maxLength) + '...';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
el.innerText = text;
|
|
23
|
+
},
|
|
24
|
+
'p-html': (el, value, modifiers = {}) => {
|
|
25
|
+
let html = value;
|
|
26
|
+
|
|
27
|
+
// Apply allow filter first (if present)
|
|
28
|
+
if (modifiers.allow && modifiers.allow.length > 0) {
|
|
29
|
+
const allowedTags = modifiers.allow;
|
|
30
|
+
const tempDiv = document.createElement('div');
|
|
31
|
+
tempDiv.innerHTML = html;
|
|
32
|
+
|
|
33
|
+
// Recursively filter elements
|
|
34
|
+
const filterNode = (node) => {
|
|
35
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
36
|
+
const tagName = node.tagName.toLowerCase();
|
|
37
|
+
if (!allowedTags.includes(tagName)) {
|
|
38
|
+
// Replace with text content
|
|
39
|
+
return document.createTextNode(node.textContent);
|
|
40
|
+
}
|
|
41
|
+
// Keep element but filter children
|
|
42
|
+
const filtered = node.cloneNode(false);
|
|
43
|
+
Array.from(node.childNodes).forEach(child => {
|
|
44
|
+
const filteredChild = filterNode(child);
|
|
45
|
+
if (filteredChild) filtered.appendChild(filteredChild);
|
|
46
|
+
});
|
|
47
|
+
return filtered;
|
|
48
|
+
}
|
|
49
|
+
return node.cloneNode();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const filtered = document.createElement('div');
|
|
53
|
+
Array.from(tempDiv.childNodes).forEach(child => {
|
|
54
|
+
const filteredChild = filterNode(child);
|
|
55
|
+
if (filteredChild) filtered.appendChild(filteredChild);
|
|
56
|
+
});
|
|
57
|
+
html = filtered.innerHTML;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Apply trim modifier (counts only text, preserves HTML tags)
|
|
61
|
+
if (modifiers.trim && modifiers.trim.length > 0) {
|
|
62
|
+
const maxLength = parseInt(modifiers.trim[0]) || 100;
|
|
63
|
+
const tempDiv = document.createElement('div');
|
|
64
|
+
tempDiv.innerHTML = html;
|
|
65
|
+
|
|
66
|
+
let charCount = 0;
|
|
67
|
+
let truncated = false;
|
|
68
|
+
|
|
69
|
+
// Recursively traverse and trim while preserving HTML structure
|
|
70
|
+
const trimNode = (node) => {
|
|
71
|
+
if (truncated) return null;
|
|
72
|
+
|
|
73
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
74
|
+
const text = node.textContent;
|
|
75
|
+
const remaining = maxLength - charCount;
|
|
76
|
+
|
|
77
|
+
if (text.length <= remaining) {
|
|
78
|
+
charCount += text.length;
|
|
79
|
+
return node.cloneNode();
|
|
80
|
+
} else {
|
|
81
|
+
// This text node exceeds limit
|
|
82
|
+
truncated = true;
|
|
83
|
+
const trimmedText = text.substring(0, remaining) + '...';
|
|
84
|
+
return document.createTextNode(trimmedText);
|
|
85
|
+
}
|
|
86
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
87
|
+
const cloned = node.cloneNode(false);
|
|
88
|
+
for (let child of node.childNodes) {
|
|
89
|
+
const trimmedChild = trimNode(child);
|
|
90
|
+
if (trimmedChild) {
|
|
91
|
+
cloned.appendChild(trimmedChild);
|
|
92
|
+
}
|
|
93
|
+
if (truncated) break;
|
|
94
|
+
}
|
|
95
|
+
return cloned;
|
|
96
|
+
}
|
|
97
|
+
return node.cloneNode();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = document.createElement('div');
|
|
101
|
+
for (let child of tempDiv.childNodes) {
|
|
102
|
+
const trimmedChild = trimNode(child);
|
|
103
|
+
if (trimmedChild) {
|
|
104
|
+
result.appendChild(trimmedChild);
|
|
105
|
+
}
|
|
106
|
+
if (truncated) break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
html = result.innerHTML;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
el.innerHTML = html;
|
|
113
|
+
},
|
|
114
|
+
'p-show': (el, value) => {
|
|
115
|
+
el.style.display = value ? '' : 'none'
|
|
116
|
+
},
|
|
117
|
+
'p-style': (el, value) => {
|
|
118
|
+
if (typeof value === 'string') {
|
|
119
|
+
el.style.cssText = value;
|
|
120
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
121
|
+
Object.assign(el.style, value);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
'p-class': (el, value) => {
|
|
125
|
+
if (typeof value === 'string') {
|
|
126
|
+
el.className = value;
|
|
127
|
+
} else if (Array.isArray(value)) {
|
|
128
|
+
el.className = value.join(' ');
|
|
129
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
130
|
+
// Object format: { className: boolean, ... }
|
|
131
|
+
el.className = Object.keys(value)
|
|
132
|
+
.filter(key => value[key])
|
|
133
|
+
.join(' ');
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
'p-model': (el, value) => {
|
|
137
|
+
el.value = value
|
|
138
|
+
},
|
|
139
|
+
'p-attr': (el, value, modifiers = {}) => {
|
|
140
|
+
// Two usage modes:
|
|
141
|
+
// 1. Single attribute: p-attr:data-id="userId" or p-attr:href="link"
|
|
142
|
+
// 2. Multiple attributes: p-attr="{ 'data-id': userId, 'data-name': userName }"
|
|
143
|
+
|
|
144
|
+
// Check if this is single attribute mode (has modifier parts)
|
|
145
|
+
const modifierKeys = Object.keys(modifiers);
|
|
146
|
+
if (modifierKeys.length > 0) {
|
|
147
|
+
// Single attribute mode: p-attr:data-id="value"
|
|
148
|
+
// The attribute name is the first modifier
|
|
149
|
+
const attrName = modifierKeys[0];
|
|
150
|
+
if (value === null || value === undefined || value === false) {
|
|
151
|
+
el.removeAttribute(attrName);
|
|
152
|
+
} else {
|
|
153
|
+
el.setAttribute(attrName, String(value));
|
|
154
|
+
}
|
|
155
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
156
|
+
// Multiple attributes mode: p-attr="{ 'data-id': userId }"
|
|
157
|
+
Object.keys(value).forEach(attrName => {
|
|
158
|
+
const attrValue = value[attrName];
|
|
159
|
+
if (attrValue === null || attrValue === undefined || attrValue === false) {
|
|
160
|
+
el.removeAttribute(attrName);
|
|
161
|
+
} else {
|
|
162
|
+
el.setAttribute(attrName, String(attrValue));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
parseDirectiveModifiers(attrName) {
|
|
170
|
+
// Parse: p-html:trim.300:allow.p.h1.h2
|
|
171
|
+
// Returns: { directive: 'p-html', modifiers: { trim: ['300'], allow: ['p', 'h1', 'h2'] } }
|
|
172
|
+
const parts = attrName.split(':');
|
|
173
|
+
const directive = parts[0];
|
|
174
|
+
const modifiers = {};
|
|
175
|
+
|
|
176
|
+
// Parse each modifier group
|
|
177
|
+
for (let i = 1; i < parts.length; i++) {
|
|
178
|
+
const modParts = parts[i].split('.');
|
|
179
|
+
const modName = modParts[0];
|
|
180
|
+
const modValues = modParts.slice(1);
|
|
181
|
+
modifiers[modName] = modValues;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { directive, modifiers };
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// ==================== INITIALIZATION ====================
|
|
188
|
+
|
|
189
|
+
async start() {
|
|
190
|
+
this.root = document.documentElement;
|
|
191
|
+
|
|
192
|
+
// Load root data (props from CMS)
|
|
193
|
+
const rootDataJsonString = document.getElementById("p-root-data")?.textContent;
|
|
194
|
+
try {
|
|
195
|
+
this.rawData = JSON.parse(rootDataJsonString || '{}');
|
|
196
|
+
} catch (e) {
|
|
197
|
+
console.error("Error parsing root data JSON:", e);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.buildScopeData(this.root, this.rawData);
|
|
201
|
+
this.data = this.observe(this.rawData);
|
|
202
|
+
this.walkDom(this.root, this.data, true);
|
|
203
|
+
this.refreshAllLoops();
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
buildScopeData(el, parentData) {
|
|
207
|
+
let currentData = parentData;
|
|
208
|
+
if (el.hasAttribute('p-scope')) {
|
|
209
|
+
const dataId = el.getAttribute('p-id') || 'missing_p-id';
|
|
210
|
+
if (!parentData._p_children) {
|
|
211
|
+
parentData._p_children = {};
|
|
212
|
+
}
|
|
213
|
+
if (!parentData._p_children[dataId]) {
|
|
214
|
+
parentData._p_children[dataId] = {};
|
|
215
|
+
}
|
|
216
|
+
currentData = parentData._p_children[dataId];
|
|
217
|
+
currentData._p_scope = el.getAttribute('p-scope');
|
|
218
|
+
}
|
|
219
|
+
let child = el.firstElementChild;
|
|
220
|
+
while (child) {
|
|
221
|
+
this.buildScopeData(child, currentData);
|
|
222
|
+
child = child.nextElementSibling;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// ==================== REACTIVE PROXY ====================
|
|
227
|
+
|
|
228
|
+
observe(data, parentScope) {
|
|
229
|
+
const localTarget = data;
|
|
230
|
+
let proxyTarget = localTarget;
|
|
231
|
+
if (parentScope) {
|
|
232
|
+
proxyTarget = Object.create(parentScope._p_target || parentScope);
|
|
233
|
+
Object.assign(proxyTarget, localTarget);
|
|
234
|
+
}
|
|
235
|
+
const proxy = new Proxy(proxyTarget, {
|
|
236
|
+
get: (target, key) => {
|
|
237
|
+
if (key === '_p_target') {
|
|
238
|
+
return target;
|
|
239
|
+
}
|
|
240
|
+
return target[key];
|
|
241
|
+
},
|
|
242
|
+
set: (target, key, value) => {
|
|
243
|
+
target[key] = value;
|
|
244
|
+
this.walkDom(this.root, this.data, false);
|
|
245
|
+
return true;
|
|
246
|
+
},
|
|
247
|
+
has: (target, key) => {
|
|
248
|
+
return key in target;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return proxy;
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// ==================== SCOPE MANAGEMENT ====================
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Creates a new scope for an element with p-scope during hydration
|
|
258
|
+
*/
|
|
259
|
+
initScope(el, parentScope) {
|
|
260
|
+
const dataId = el.getAttribute('p-id');
|
|
261
|
+
const localRawData = parentScope._p_target._p_children[dataId];
|
|
262
|
+
|
|
263
|
+
// Create new inherited Proxy
|
|
264
|
+
const scope = this.observe(localRawData, parentScope);
|
|
265
|
+
|
|
266
|
+
// Execute p-scope assignments directly on target to avoid triggering setter
|
|
267
|
+
this.executePScopeStatements(scope, localRawData._p_scope);
|
|
268
|
+
|
|
269
|
+
// Initialize parent snapshot so first refresh doesn't think everything changed
|
|
270
|
+
const parentProto = Object.getPrototypeOf(scope._p_target);
|
|
271
|
+
el._parentSnapshot = {};
|
|
272
|
+
for (let key in parentProto) {
|
|
273
|
+
if (!key.startsWith('_p_')) {
|
|
274
|
+
el._parentSnapshot[key] = parentProto[key];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Initialize local snapshot for tracking local variable changes
|
|
279
|
+
const target = scope._p_target;
|
|
280
|
+
el._localSnapshot = {};
|
|
281
|
+
for (let key of Object.keys(target)) {
|
|
282
|
+
if (!key.startsWith('_p_')) {
|
|
283
|
+
el._localSnapshot[key] = target[key];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return scope;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Gets the stored scope for an element during refresh, re-executing p-scope if parent changed
|
|
292
|
+
*/
|
|
293
|
+
refreshScope(el, parentScope) {
|
|
294
|
+
let scope = el._scope;
|
|
295
|
+
|
|
296
|
+
if (!scope) {
|
|
297
|
+
return parentScope;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check if parent values changed - if so, selectively re-execute p-scope
|
|
301
|
+
const pScopeExpr = el.getAttribute('p-scope');
|
|
302
|
+
if (pScopeExpr && scope._p_target) {
|
|
303
|
+
this.updateScopeFromParent(el, scope, pScopeExpr);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return scope;
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Parses and executes p-scope statements sequentially, setting values directly on target
|
|
311
|
+
* Each statement sees the results of previous statements
|
|
312
|
+
*/
|
|
313
|
+
executePScopeStatements(scope, pScopeExpr) {
|
|
314
|
+
const statements = pScopeExpr.split(';').map(s => s.trim()).filter(s => s);
|
|
315
|
+
const target = scope._p_target;
|
|
316
|
+
|
|
317
|
+
// Create a sequential scope that always reads from target first (for updated values)
|
|
318
|
+
// then falls back to the parent scope for inherited values
|
|
319
|
+
const sequentialScope = new Proxy(target, {
|
|
320
|
+
get: (t, key) => {
|
|
321
|
+
// First check if we have a local value (possibly updated by previous statement)
|
|
322
|
+
if (Object.prototype.hasOwnProperty.call(t, key)) {
|
|
323
|
+
return t[key];
|
|
324
|
+
}
|
|
325
|
+
// Fall back to parent scope via prototype chain or original scope
|
|
326
|
+
return scope[key];
|
|
327
|
+
},
|
|
328
|
+
has: (t, key) => {
|
|
329
|
+
return Object.prototype.hasOwnProperty.call(t, key) || key in scope;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
for (const stmt of statements) {
|
|
334
|
+
const match = stmt.match(/^(\w+)\s*=\s*(.+)$/);
|
|
335
|
+
if (match) {
|
|
336
|
+
const [, varName, expr] = match;
|
|
337
|
+
try {
|
|
338
|
+
const value = eval(`with (sequentialScope) { (${expr}) }`);
|
|
339
|
+
target[varName] = value;
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.error(`Error executing p-scope statement "${stmt}":`, e);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Re-executes p-scope statements that depend on changed variables (parent or local)
|
|
349
|
+
* Statements are executed sequentially so each sees results of previous ones
|
|
350
|
+
*/
|
|
351
|
+
updateScopeFromParent(el, scope, pScopeExpr) {
|
|
352
|
+
const parentProto = Object.getPrototypeOf(scope._p_target);
|
|
353
|
+
const target = scope._p_target;
|
|
354
|
+
|
|
355
|
+
// Track which variables changed in PARENT vs LOCAL separately
|
|
356
|
+
const changedParentVars = new Set();
|
|
357
|
+
const changedLocalVars = new Set();
|
|
358
|
+
|
|
359
|
+
// Check parent variable changes
|
|
360
|
+
if (!el._parentSnapshot) {
|
|
361
|
+
el._parentSnapshot = {};
|
|
362
|
+
}
|
|
363
|
+
for (let key in parentProto) {
|
|
364
|
+
if (!key.startsWith('_p_')) {
|
|
365
|
+
if (el._parentSnapshot[key] !== parentProto[key]) {
|
|
366
|
+
changedParentVars.add(key);
|
|
367
|
+
}
|
|
368
|
+
el._parentSnapshot[key] = parentProto[key];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check local variable changes (variables that are OWN properties of target)
|
|
373
|
+
if (!el._localSnapshot) {
|
|
374
|
+
el._localSnapshot = {};
|
|
375
|
+
}
|
|
376
|
+
for (let key of Object.keys(target)) {
|
|
377
|
+
if (!key.startsWith('_p_')) {
|
|
378
|
+
if (el._localSnapshot[key] !== target[key]) {
|
|
379
|
+
changedLocalVars.add(key);
|
|
380
|
+
}
|
|
381
|
+
el._localSnapshot[key] = target[key];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Combine for checking which statements to execute
|
|
386
|
+
const allChangedVars = new Set([...changedParentVars, ...changedLocalVars]);
|
|
387
|
+
|
|
388
|
+
if (allChangedVars.size === 0) return;
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const statements = pScopeExpr.split(';').map(s => s.trim()).filter(s => s);
|
|
392
|
+
|
|
393
|
+
// Identify OUTPUT variables (variables SET by p-scope statements)
|
|
394
|
+
// Local changes to these should NOT trigger re-execution
|
|
395
|
+
const outputVars = new Set();
|
|
396
|
+
statements.forEach(stmt => {
|
|
397
|
+
const match = stmt.match(/^(\w+)\s*=\s*(.+)$/);
|
|
398
|
+
if (match) {
|
|
399
|
+
outputVars.add(match[1]);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Track variables set during THIS re-execution pass
|
|
404
|
+
const setInThisPass = new Set();
|
|
405
|
+
|
|
406
|
+
// Create a sequential scope that:
|
|
407
|
+
// - Uses values from this pass if already computed
|
|
408
|
+
// - For PARENT-changed vars: reads from parent (new value)
|
|
409
|
+
// - For LOCAL-changed vars: reads from local (new value)
|
|
410
|
+
// - Otherwise: reads from local if exists, else parent
|
|
411
|
+
const sequentialScope = new Proxy(target, {
|
|
412
|
+
get: (t, key) => {
|
|
413
|
+
if (key === '_p_target' || key === '_p_children' || key === '_p_scope') {
|
|
414
|
+
return t[key];
|
|
415
|
+
}
|
|
416
|
+
// If this variable was set by a previous statement in this pass, use that
|
|
417
|
+
if (setInThisPass.has(key)) {
|
|
418
|
+
return t[key];
|
|
419
|
+
}
|
|
420
|
+
// If this variable changed in PARENT, read from parent (new value)
|
|
421
|
+
if (changedParentVars.has(key)) {
|
|
422
|
+
return parentProto[key];
|
|
423
|
+
}
|
|
424
|
+
// If this variable changed LOCALLY, read from local (new value)
|
|
425
|
+
if (changedLocalVars.has(key)) {
|
|
426
|
+
return t[key];
|
|
427
|
+
}
|
|
428
|
+
// Otherwise, read current value (local if exists, else parent)
|
|
429
|
+
if (Object.prototype.hasOwnProperty.call(t, key)) {
|
|
430
|
+
return t[key];
|
|
431
|
+
}
|
|
432
|
+
return parentProto[key];
|
|
433
|
+
},
|
|
434
|
+
set: (t, key, value) => {
|
|
435
|
+
t[key] = value;
|
|
436
|
+
return true;
|
|
437
|
+
},
|
|
438
|
+
has: (t, key) => {
|
|
439
|
+
return setInThisPass.has(key) || Object.prototype.hasOwnProperty.call(t, key) || key in parentProto;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
statements.forEach(stmt => {
|
|
444
|
+
let shouldExecute = false;
|
|
445
|
+
const parts = stmt.split('=');
|
|
446
|
+
if (parts.length <= 1) return;
|
|
447
|
+
const rhs = parts.slice(1).join('=');
|
|
448
|
+
|
|
449
|
+
// Execute if RHS contains a PARENT-changed variable
|
|
450
|
+
changedParentVars.forEach(varName => {
|
|
451
|
+
if (rhs.includes(varName)) {
|
|
452
|
+
shouldExecute = true;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Execute if RHS contains a LOCAL-changed variable that is NOT an output var
|
|
457
|
+
// (Local changes to output vars are user modifications that should stick)
|
|
458
|
+
changedLocalVars.forEach(varName => {
|
|
459
|
+
if (!outputVars.has(varName) && rhs.includes(varName)) {
|
|
460
|
+
shouldExecute = true;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Also execute if the statement depends on a variable set earlier in this pass
|
|
465
|
+
if (!shouldExecute) {
|
|
466
|
+
setInThisPass.forEach(varName => {
|
|
467
|
+
if (rhs.includes(varName)) {
|
|
468
|
+
shouldExecute = true;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (shouldExecute) {
|
|
474
|
+
const match = stmt.match(/^(\w+)\s*=\s*(.+)$/);
|
|
475
|
+
if (match) {
|
|
476
|
+
const [, varName, expr] = match;
|
|
477
|
+
const value = eval(`with (sequentialScope) { (${expr}) }`);
|
|
478
|
+
target[varName] = value;
|
|
479
|
+
setInThisPass.add(varName);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Update local snapshot with any newly computed values
|
|
485
|
+
for (let key of Object.keys(target)) {
|
|
486
|
+
if (!key.startsWith('_p_')) {
|
|
487
|
+
el._localSnapshot[key] = target[key];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
console.error(`Error re-executing p-scope expression:`, e);
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
// ==================== EVENT HANDLING ====================
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Registers p-on:* event listeners on an element
|
|
499
|
+
*/
|
|
500
|
+
registerEventListeners(el) {
|
|
501
|
+
Array.from(el.attributes).forEach(attr => {
|
|
502
|
+
if (attr.name.startsWith('p-on:')) {
|
|
503
|
+
const event = attr.name.replace('p-on:', '');
|
|
504
|
+
el.addEventListener(event, () => {
|
|
505
|
+
eval(`with (el._scope) { ${attr.value} }`);
|
|
506
|
+
this.refreshAllLoops();
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Sets up p-model two-way binding on an element
|
|
514
|
+
*/
|
|
515
|
+
registerModelBinding(el) {
|
|
516
|
+
const modelAttr = el.getAttribute('p-model');
|
|
517
|
+
if (!modelAttr) return;
|
|
518
|
+
|
|
519
|
+
el.addEventListener('input', (e) => {
|
|
520
|
+
let value;
|
|
521
|
+
const type = e.target.type;
|
|
522
|
+
|
|
523
|
+
if (type === 'number' || type === 'range') {
|
|
524
|
+
value = e.target.value === '' ? null : Number(e.target.value);
|
|
525
|
+
} else if (type === 'checkbox') {
|
|
526
|
+
value = e.target.checked;
|
|
527
|
+
} else if (type === 'radio') {
|
|
528
|
+
value = e.target.checked ? e.target.value : undefined;
|
|
529
|
+
} else {
|
|
530
|
+
value = e.target.value;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (value !== undefined) {
|
|
534
|
+
eval(`with (el._scope) { ${modelAttr} = value }`);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// For checkbox and radio, also listen to 'change' event
|
|
539
|
+
if (el.type === 'checkbox' || el.type === 'radio') {
|
|
540
|
+
el.addEventListener('change', (e) => {
|
|
541
|
+
let value;
|
|
542
|
+
if (el.type === 'checkbox') {
|
|
543
|
+
value = e.target.checked;
|
|
544
|
+
} else {
|
|
545
|
+
value = e.target.checked ? e.target.value : undefined;
|
|
546
|
+
}
|
|
547
|
+
if (value !== undefined) {
|
|
548
|
+
eval(`with (el._scope) { ${modelAttr} = value }`);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
// ==================== DIRECTIVE EVALUATION ====================
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Evaluates all directives on an element
|
|
558
|
+
*/
|
|
559
|
+
evaluateDirectives(el, scope) {
|
|
560
|
+
Array.from(el.attributes).forEach(attr => {
|
|
561
|
+
const parsed = this.parseDirectiveModifiers(attr.name);
|
|
562
|
+
if (Object.keys(this.directives).includes(parsed.directive)) {
|
|
563
|
+
const evalScope = el._scope || scope;
|
|
564
|
+
const value = eval(`with (evalScope) { (${attr.value}) }`);
|
|
565
|
+
this.directives[parsed.directive](el, value, parsed.modifiers);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
// ==================== LOOP HANDLING ====================
|
|
571
|
+
|
|
572
|
+
setForTemplateRecursive(element, template) {
|
|
573
|
+
element._forTemplate = template;
|
|
574
|
+
Array.from(element.children).forEach(child => {
|
|
575
|
+
this.setForTemplateRecursive(child, template);
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
refreshAllLoops(el = this.root, processedTemplates = new Set()) {
|
|
580
|
+
if (el.tagName === 'TEMPLATE' && el.hasAttribute('p-for')) {
|
|
581
|
+
// Skip if this template was already processed during this refresh cycle
|
|
582
|
+
// (e.g., as a nested template during an outer loop's refresh)
|
|
583
|
+
if (processedTemplates.has(el)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
processedTemplates.add(el);
|
|
587
|
+
this.handleFor(el, el._scope || this.data, false);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let child = el.firstElementChild;
|
|
592
|
+
while (child) {
|
|
593
|
+
const nextChild = child.nextElementSibling; // Store before processing in case DOM changes
|
|
594
|
+
this.refreshAllLoops(child, processedTemplates);
|
|
595
|
+
child = nextChild;
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
handleFor(template, parentScope, isHydrating) {
|
|
600
|
+
const forExpr = template.getAttribute('p-for');
|
|
601
|
+
const match = forExpr.match(/^(?:const|let)?\s*(.+?)\s+(of|in)\s+(.+)$/);
|
|
602
|
+
|
|
603
|
+
if (!match) {
|
|
604
|
+
console.error(`Invalid p-for expression: ${forExpr}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const [, varPattern, operator, iterableExpr] = match;
|
|
609
|
+
|
|
610
|
+
if (isHydrating) {
|
|
611
|
+
this.hydrateLoop(template, parentScope, varPattern, iterableExpr);
|
|
612
|
+
} else {
|
|
613
|
+
this.refreshLoop(template, parentScope, varPattern, iterableExpr);
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Gets the scope prefix for a template based on its nesting level
|
|
619
|
+
* Checks ancestors and siblings for parent p-for templates to build hierarchical key
|
|
620
|
+
*/
|
|
621
|
+
getTemplateScopePrefix(template) {
|
|
622
|
+
let ancestorKey = '';
|
|
623
|
+
|
|
624
|
+
// First, check if this template has _forTemplate set (meaning it was rendered by an outer loop)
|
|
625
|
+
// This is the most reliable indicator for nested templates
|
|
626
|
+
if (template._forTemplate) {
|
|
627
|
+
const parentForData = template._forTemplate._forData;
|
|
628
|
+
if (parentForData && parentForData.scopePrefix) {
|
|
629
|
+
// Find which iteration we're in by checking sibling elements
|
|
630
|
+
let sibling = template.previousElementSibling;
|
|
631
|
+
while (sibling) {
|
|
632
|
+
if (sibling.hasAttribute && sibling.hasAttribute('p-for-key')) {
|
|
633
|
+
const siblingKey = sibling.getAttribute('p-for-key');
|
|
634
|
+
ancestorKey = siblingKey + '-';
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
sibling = sibling.previousElementSibling;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// If no _forTemplate, check previous siblings for p-for-key
|
|
643
|
+
// This handles the case where template is adjacent to loop-rendered elements
|
|
644
|
+
if (!ancestorKey) {
|
|
645
|
+
let sibling = template.previousElementSibling;
|
|
646
|
+
while (sibling) {
|
|
647
|
+
if (sibling.hasAttribute && sibling.hasAttribute('p-for-key')) {
|
|
648
|
+
// Check if this sibling shares the same parent template
|
|
649
|
+
if (sibling._forTemplate === template._forTemplate) {
|
|
650
|
+
const siblingKey = sibling.getAttribute('p-for-key');
|
|
651
|
+
ancestorKey = siblingKey + '-';
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
sibling = sibling.previousElementSibling;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Also check parent elements for p-for-key
|
|
660
|
+
if (!ancestorKey) {
|
|
661
|
+
let parent = template.parentElement;
|
|
662
|
+
while (parent) {
|
|
663
|
+
if (parent.hasAttribute && parent.hasAttribute('p-for-key')) {
|
|
664
|
+
const parentKey = parent.getAttribute('p-for-key');
|
|
665
|
+
ancestorKey = parentKey + '-';
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
if (parent._forTemplate) {
|
|
669
|
+
const parentForData = parent._forTemplate._forData;
|
|
670
|
+
if (parentForData) {
|
|
671
|
+
const parentIndex = parentForData.renderedElements.indexOf(parent);
|
|
672
|
+
if (parentIndex >= 0) {
|
|
673
|
+
ancestorKey = (parentForData.scopePrefix || '') + parentIndex + '-';
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
parent = parent.parentElement;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Generate a unique scope ID for this template
|
|
683
|
+
if (!template._forScopeId) {
|
|
684
|
+
template._forScopeId = 's' + (this._templateScopeCounter++);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return ancestorKey + template._forScopeId + ':';
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
hydrateLoop(template, parentScope, varPattern, iterableExpr) {
|
|
691
|
+
template._scope = parentScope;
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
const iterable = eval(`with (parentScope) { (${iterableExpr}) }`);
|
|
695
|
+
|
|
696
|
+
// Get the scope prefix for this template's keys
|
|
697
|
+
const scopePrefix = this.getTemplateScopePrefix(template);
|
|
698
|
+
|
|
699
|
+
template._forData = {
|
|
700
|
+
varPattern,
|
|
701
|
+
iterableExpr,
|
|
702
|
+
renderedElements: [],
|
|
703
|
+
scopePrefix
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Check for SSR-rendered elements - only match those with our scope prefix
|
|
707
|
+
const existingElementsByKey = {};
|
|
708
|
+
let sibling = template.nextElementSibling;
|
|
709
|
+
while (sibling && sibling.hasAttribute('p-for-key')) {
|
|
710
|
+
const fullKey = sibling.getAttribute('p-for-key');
|
|
711
|
+
|
|
712
|
+
// Check if this element belongs to this template (has our scope prefix)
|
|
713
|
+
// or is an old-style unscoped key (for backwards compatibility)
|
|
714
|
+
const isOurElement = fullKey.startsWith(scopePrefix) ||
|
|
715
|
+
(!fullKey.includes(':') && !fullKey.includes('-')); // unscoped legacy key
|
|
716
|
+
|
|
717
|
+
if (isOurElement) {
|
|
718
|
+
// Extract the index part from the key
|
|
719
|
+
let indexKey;
|
|
720
|
+
if (fullKey.startsWith(scopePrefix)) {
|
|
721
|
+
indexKey = fullKey.substring(scopePrefix.length);
|
|
722
|
+
} else {
|
|
723
|
+
indexKey = fullKey; // legacy unscoped key
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (!existingElementsByKey[indexKey]) {
|
|
727
|
+
existingElementsByKey[indexKey] = [];
|
|
728
|
+
}
|
|
729
|
+
existingElementsByKey[indexKey].push(sibling);
|
|
730
|
+
}
|
|
731
|
+
sibling = sibling.nextElementSibling;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let index = 0;
|
|
735
|
+
let lastInserted = template;
|
|
736
|
+
|
|
737
|
+
for (const item of iterable) {
|
|
738
|
+
const loopScope = this.createLoopScope(parentScope, varPattern, item);
|
|
739
|
+
|
|
740
|
+
if (existingElementsByKey[String(index)]) {
|
|
741
|
+
// Hydrate existing SSR elements
|
|
742
|
+
existingElementsByKey[String(index)].forEach(el => {
|
|
743
|
+
el._scope = loopScope;
|
|
744
|
+
this.setForTemplateRecursive(el, template);
|
|
745
|
+
// Update key to use scoped format
|
|
746
|
+
if (el.tagName !== 'TEMPLATE') {
|
|
747
|
+
el.setAttribute('p-for-key', scopePrefix + index);
|
|
748
|
+
}
|
|
749
|
+
this.walkDom(el, loopScope, true);
|
|
750
|
+
template._forData.renderedElements.push(el);
|
|
751
|
+
lastInserted = el;
|
|
752
|
+
});
|
|
753
|
+
} else {
|
|
754
|
+
// Render from template
|
|
755
|
+
const clone = template.content.cloneNode(true);
|
|
756
|
+
const elements = Array.from(clone.children);
|
|
757
|
+
|
|
758
|
+
elements.forEach(el => {
|
|
759
|
+
el._scope = loopScope;
|
|
760
|
+
this.setForTemplateRecursive(el, template);
|
|
761
|
+
// Only set p-for-key on non-template elements at this level
|
|
762
|
+
if (el.tagName !== 'TEMPLATE') {
|
|
763
|
+
el.setAttribute('p-for-key', scopePrefix + index);
|
|
764
|
+
}
|
|
765
|
+
this.walkDom(el, loopScope, true);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Insert elements after the last inserted element
|
|
769
|
+
const fragment = document.createDocumentFragment();
|
|
770
|
+
elements.forEach(el => fragment.appendChild(el));
|
|
771
|
+
lastInserted.parentNode.insertBefore(fragment, lastInserted.nextSibling);
|
|
772
|
+
|
|
773
|
+
template._forData.renderedElements.push(...elements);
|
|
774
|
+
lastInserted = elements[elements.length - 1] || lastInserted;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
index++;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Remove extra SSR elements
|
|
781
|
+
Object.keys(existingElementsByKey).forEach(key => {
|
|
782
|
+
if (parseInt(key) >= index) {
|
|
783
|
+
existingElementsByKey[key].forEach(el => el.remove());
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
} catch (e) {
|
|
787
|
+
console.error(`Error in p-for hydration: ${iterableExpr}`, e);
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Recursively removes elements and their nested loop contents
|
|
793
|
+
*/
|
|
794
|
+
removeLoopElements(elements) {
|
|
795
|
+
elements.forEach(el => {
|
|
796
|
+
// If this element is a template with its own rendered elements, remove those first
|
|
797
|
+
if (el._forData && el._forData.renderedElements) {
|
|
798
|
+
this.removeLoopElements(el._forData.renderedElements);
|
|
799
|
+
el._forData.renderedElements = [];
|
|
800
|
+
}
|
|
801
|
+
// Also check children for templates with rendered elements
|
|
802
|
+
if (el.querySelectorAll) {
|
|
803
|
+
const nestedTemplates = el.querySelectorAll('template[p-for]');
|
|
804
|
+
nestedTemplates.forEach(tpl => {
|
|
805
|
+
if (tpl._forData && tpl._forData.renderedElements) {
|
|
806
|
+
this.removeLoopElements(tpl._forData.renderedElements);
|
|
807
|
+
tpl._forData.renderedElements = [];
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
el.remove();
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
refreshLoop(template, parentScope, varPattern, iterableExpr) {
|
|
816
|
+
const forData = template._forData;
|
|
817
|
+
if (!forData) return;
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const iterable = eval(`with (parentScope._p_target || parentScope) { (${iterableExpr}) }`);
|
|
821
|
+
|
|
822
|
+
// Use stored scope prefix or regenerate if needed
|
|
823
|
+
const scopePrefix = forData.scopePrefix || this.getTemplateScopePrefix(template);
|
|
824
|
+
|
|
825
|
+
// Remove all elements (including nested loop elements) and re-render
|
|
826
|
+
this.removeLoopElements(forData.renderedElements);
|
|
827
|
+
forData.renderedElements = [];
|
|
828
|
+
|
|
829
|
+
// Track the actual last inserted element (accounts for nested template output)
|
|
830
|
+
let lastInsertedElement = template;
|
|
831
|
+
|
|
832
|
+
let index = 0;
|
|
833
|
+
for (const item of iterable) {
|
|
834
|
+
const clone = template.content.cloneNode(true);
|
|
835
|
+
const loopScope = this.createLoopScope(parentScope, forData.varPattern, item);
|
|
836
|
+
|
|
837
|
+
const elements = Array.from(clone.children);
|
|
838
|
+
|
|
839
|
+
// First pass: set up scope and basic attributes (but don't process nested templates yet)
|
|
840
|
+
elements.forEach(el => {
|
|
841
|
+
el._scope = loopScope;
|
|
842
|
+
this.setForTemplateRecursive(el, template);
|
|
843
|
+
// Only set p-for-key on non-template elements
|
|
844
|
+
if (el.tagName !== 'TEMPLATE') {
|
|
845
|
+
el.setAttribute('p-for-key', scopePrefix + index);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// Insert elements into DOM after the last inserted element
|
|
850
|
+
const fragment = document.createDocumentFragment();
|
|
851
|
+
elements.forEach(el => fragment.appendChild(el));
|
|
852
|
+
lastInsertedElement.parentNode.insertBefore(fragment, lastInsertedElement.nextSibling);
|
|
853
|
+
forData.renderedElements.push(...elements);
|
|
854
|
+
|
|
855
|
+
// Second pass: now that elements are in DOM, process them (including nested templates)
|
|
856
|
+
elements.forEach(el => {
|
|
857
|
+
this.walkDom(el, loopScope, true);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// Update lastInsertedElement to the actual last element in the DOM from this iteration
|
|
861
|
+
// This accounts for nested template rendered elements
|
|
862
|
+
const lastElement = elements[elements.length - 1];
|
|
863
|
+
if (lastElement) {
|
|
864
|
+
lastInsertedElement = this.findLastRenderedSibling(lastElement);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
index++;
|
|
868
|
+
}
|
|
869
|
+
} catch (e) {
|
|
870
|
+
console.error(`Error in p-for refresh: ${iterableExpr}`, e);
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Finds the last element rendered by a template (including nested template output)
|
|
876
|
+
*/
|
|
877
|
+
findLastRenderedSibling(element) {
|
|
878
|
+
// If this is a p-for template with rendered elements, get the last rendered element
|
|
879
|
+
if (element.tagName === 'TEMPLATE' && element._forData && element._forData.renderedElements.length > 0) {
|
|
880
|
+
const lastRendered = element._forData.renderedElements[element._forData.renderedElements.length - 1];
|
|
881
|
+
return this.findLastRenderedSibling(lastRendered);
|
|
882
|
+
}
|
|
883
|
+
return element;
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
createLoopScope(parentScope, varPattern, item) {
|
|
887
|
+
const loopData = {};
|
|
888
|
+
varPattern = varPattern.trim();
|
|
889
|
+
|
|
890
|
+
if (varPattern.startsWith('[')) {
|
|
891
|
+
// Array destructuring: [i, cat]
|
|
892
|
+
const vars = varPattern.slice(1, -1).split(',').map(v => v.trim());
|
|
893
|
+
if (Array.isArray(item)) {
|
|
894
|
+
vars.forEach((v, i) => loopData[v] = item[i]);
|
|
895
|
+
} else {
|
|
896
|
+
loopData[vars[0]] = item;
|
|
897
|
+
}
|
|
898
|
+
} else if (varPattern.startsWith('{')) {
|
|
899
|
+
// Object destructuring: {name, age}
|
|
900
|
+
const vars = varPattern.slice(1, -1).split(',').map(v => v.trim());
|
|
901
|
+
vars.forEach(v => {
|
|
902
|
+
const [key, alias] = v.split(':').map(s => s.trim());
|
|
903
|
+
loopData[alias || key] = item[key];
|
|
904
|
+
});
|
|
905
|
+
} else {
|
|
906
|
+
// Simple variable
|
|
907
|
+
loopData[varPattern] = item;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!parentScope) {
|
|
911
|
+
console.error('parentScope is undefined in createLoopScope');
|
|
912
|
+
return new Proxy({}, { get: () => undefined, set: () => false });
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const parentTarget = parentScope._p_target || parentScope;
|
|
916
|
+
if (!parentTarget || typeof parentTarget !== 'object') {
|
|
917
|
+
console.error('Invalid parentTarget:', parentTarget);
|
|
918
|
+
return new Proxy(loopData, {
|
|
919
|
+
get: (target, key) => target[key],
|
|
920
|
+
set: (target, key, value) => { target[key] = value; return true; }
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const loopTarget = Object.create(parentTarget);
|
|
925
|
+
Object.assign(loopTarget, loopData);
|
|
926
|
+
|
|
927
|
+
const proxy = new Proxy(loopTarget, {
|
|
928
|
+
get: (target, key) => target[key],
|
|
929
|
+
set: (target, key, value) => {
|
|
930
|
+
if (key in loopData) {
|
|
931
|
+
target[key] = value;
|
|
932
|
+
} else {
|
|
933
|
+
parentTarget[key] = value;
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
proxy._p_target = loopTarget;
|
|
939
|
+
|
|
940
|
+
return proxy;
|
|
941
|
+
},
|
|
942
|
+
|
|
943
|
+
// ==================== MAIN DOM WALKER ====================
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Walks the DOM tree, processing scopes, event handlers, and directives
|
|
947
|
+
*/
|
|
948
|
+
walkDom(el, parentScope, isHydrating = false) {
|
|
949
|
+
// Handle p-for templates separately
|
|
950
|
+
if (el.tagName === 'TEMPLATE' && el.hasAttribute('p-for')) {
|
|
951
|
+
// Skip nested templates during refresh (non-hydrating) - they will be
|
|
952
|
+
// re-created by their parent loop. Their scope variables come from the
|
|
953
|
+
// parent loop iteration, not the static scope chain.
|
|
954
|
+
if (!isHydrating && el._forTemplate) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this.handleFor(el, parentScope, isHydrating);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Determine the scope for this element
|
|
962
|
+
let currentScope = parentScope;
|
|
963
|
+
|
|
964
|
+
if (el.hasAttribute('p-scope')) {
|
|
965
|
+
if (isHydrating) {
|
|
966
|
+
currentScope = this.initScope(el, parentScope);
|
|
967
|
+
} else {
|
|
968
|
+
currentScope = this.refreshScope(el, parentScope);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Store scope on element during hydration
|
|
973
|
+
if (isHydrating) {
|
|
974
|
+
el._scope = currentScope;
|
|
975
|
+
this.registerEventListeners(el);
|
|
976
|
+
this.registerModelBinding(el);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Evaluate directives
|
|
980
|
+
if (currentScope) {
|
|
981
|
+
this.evaluateDirectives(el, currentScope);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Recurse to children
|
|
985
|
+
// Use Array.from to capture a snapshot of children, avoiding issues where
|
|
986
|
+
// p-for templates insert new elements during the walk
|
|
987
|
+
const children = Array.from(el.children);
|
|
988
|
+
for (const child of children) {
|
|
989
|
+
this.walkDom(child, currentScope, isHydrating);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
window.Pattr.start()
|
|
997
|
+
|