@jinntec/fore 2.9.0 → 3.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "2.9.0",
3
+ "version": "3.0.1",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
@@ -37,6 +37,8 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@babel/plugin-proposal-class-properties": "^7.17.12",
40
+ "@babel/plugin-proposal-private-methods": "^7.18.6",
41
+ "@babel/plugin-transform-private-methods": "^7.28.6",
40
42
  "@jinntec/jinn-codemirror": "^1.17.6",
41
43
  "@open-wc/building-rollup": "^2.0.1",
42
44
  "@open-wc/eslint-config": "^7.0.0",
@@ -21,7 +21,7 @@ export default class DependentXPathQueries {
21
21
  }
22
22
  }
23
23
 
24
- // We can also depend on the index function if it was used in our ancestry
24
+ // We can also depend on the index functioxn if it was used in our ancestry
25
25
  return !!this._parentDependencies?.isInvalidatedByIndexFunction();
26
26
  }
27
27
 
@@ -57,13 +57,48 @@ export default class DependentXPathQueries {
57
57
  return !!this._parentDependencies?.isInvalidatedByChildlistChanges(affectedLocalNames);
58
58
  }
59
59
 
60
+ /**
61
+ * Add an XPath to the dependencies
62
+ *
63
+ * @param {string} xpath the XPath to add
64
+ */
60
65
  /**
61
66
  * Add an XPath to the dependencies
62
67
  *
63
68
  * @param {string} xpath the XPath to add
64
69
  */
65
70
  addXPath(xpath) {
66
- this._xpaths.add(xpath);
71
+ const expr = String(xpath ?? '');
72
+ if (!expr) return;
73
+
74
+ // Always keep the original expression
75
+ this._xpaths.add(expr);
76
+
77
+ // --- NEW: extract implicit JSON lookup deps hidden behind variables ---
78
+ //
79
+ // Examples:
80
+ // contains(., $default?ui?query) -> adds ?ui?query
81
+ // $foo.?ui?query -> adds ?ui?query
82
+ //
83
+ // We only add lookup tails that start with ? or .? and then a key.
84
+ // This is intentionally conservative and string-based.
85
+ const varLookupRe =
86
+ /\$[A-Za-z_][\w.-]*\s*(\.\?)?(\?[\w$.-]+(?:\[[^\]]+\])?)(\?[\w$.-]+(?:\[[^\]]+\])?)*(\?\*)?/g;
87
+
88
+ // We want the full tail beginning at the first '?' (ignore optional '.')
89
+ // so: ".?ui?query" => "?ui?query"
90
+ let m;
91
+ while ((m = varLookupRe.exec(expr)) !== null) {
92
+ const fullMatch = m[0] || '';
93
+ const qpos = fullMatch.indexOf('?');
94
+ if (qpos === -1) continue;
95
+
96
+ const tail = fullMatch.slice(qpos); // e.g. "?ui?query"
97
+ // Only record meaningful deps (ignore just "?*")
98
+ if (tail === '?*') continue;
99
+
100
+ this._xpaths.add(tail);
101
+ }
67
102
  }
68
103
 
69
104
  /**
@@ -184,7 +184,7 @@ export default class ForeElementMixin extends HTMLElement {
184
184
  // console.log('match ', match);
185
185
  const naked = match.substring(1, match.length - 1);
186
186
  const inscope = getInScopeContext(node, naked);
187
- const result = evaluateXPathToString(naked, inscope, this);
187
+ const result = evaluateXPathToString(naked, inscope, this.getOwnerForm());
188
188
  const replaced = expr.replaceAll(match, result);
189
189
  // console.log('replacing ', expr, ' with ', replaced);
190
190
  expr = replaced;
@@ -239,63 +239,89 @@ export default class ForeElementMixin extends HTMLElement {
239
239
  return null;
240
240
  }
241
241
 
242
+ /**
243
+ * @returns {import('./modelitem.js').ModelItem}
244
+ */
242
245
  /**
243
246
  * @returns {import('./modelitem.js').ModelItem}
244
247
  */
245
248
  getModelItem() {
246
- if (!this.getModel()) return;
249
+ if (!this.getModel()) return null;
247
250
 
248
- // First try to find by node reference
249
- const mi = this.getModel().getModelItem(this.nodeset);
250
- if (mi) {
251
- this.modelItem = mi;
252
- }
251
+ const model = this.getModel();
253
252
 
253
+ // Resolve the effective bound node for repeated contexts
254
254
  const repeated = XPathUtil.getClosest('fx-repeatitem', this);
255
- let existed;
255
+ let effectiveNode = this.nodeset;
256
+
256
257
  if (repeated) {
257
258
  const { index } = repeated;
258
- if (Array.isArray(this.nodeset)) {
259
- existed = this.getModel().getModelItem(this.nodeset[index - 1]);
260
- } else {
261
- existed = this.getModel().getModelItem(this.nodeset);
259
+ if (Array.isArray(effectiveNode)) {
260
+ effectiveNode = effectiveNode[index - 1];
262
261
  }
263
- } else {
264
- existed = this.nodeset ? this.getModel().getModelItem(this.nodeset) : null;
265
262
  }
266
263
 
267
- // If we couldn't find by node reference, try to find by path
268
- if (!existed && this.nodeset) {
269
- // Get the path for the current nodeset
270
- const instanceId = XPathUtil.resolveInstance(this, this.ref);
271
- let targetNode =
272
- this.nodeset.nodeType === Node.TEXT_NODE ? this.nodeset.parentNode : this.nodeset;
264
+ // 1) Try exact lookup by node OR lens object (model.getModelItem was updated earlier)
265
+ let existed = effectiveNode ? model.getModelItem(effectiveNode) : null;
266
+ if (existed) {
267
+ this.modelItem = existed;
268
+ return existed;
269
+ }
270
+
271
+ // 2) Try lookup by canonical path (XML + JSON)
272
+ const instanceId = XPathUtil.resolveInstance(this, this.ref);
273
273
 
274
- if (targetNode?.nodeType) {
275
- const path = getPath(targetNode, instanceId);
274
+ // Normalize XML text node -> parent
275
+ let targetNode = effectiveNode;
276
+ if (targetNode?.nodeType === Node.TEXT_NODE) targetNode = targetNode.parentNode;
276
277
 
277
- // Try to find a ModelItem with this path
278
- existed = this.getModel().modelItems.find(item => item.path === path);
278
+ let path = null;
279
279
 
280
- if (existed) {
281
- // Update the node reference in the existing ModelItem
280
+ // XML node path
281
+ if (targetNode?.nodeType) {
282
+ path = getPath(targetNode, instanceId);
283
+ }
284
+ // JSON lens node path (preferred)
285
+ else if (targetNode?.__jsonlens__ && typeof targetNode.getPath === 'function') {
286
+ // JSONNode.getPath() already returns the canonical path you want
287
+ path = targetNode.getPath();
288
+ }
289
+ // As a last resort: try getPath() util for JSON lens nodes if it supports them
290
+ else if (targetNode?.__jsonlens__) {
291
+ try {
292
+ path = getPath(targetNode, instanceId);
293
+ } catch (_e) {
294
+ // ignore
295
+ }
296
+ }
297
+
298
+ if (path) {
299
+ existed = model.modelItems.find(item => item.path === path) || null;
300
+ if (existed) {
301
+ // CRITICAL: retarget existing ModelItem to the current backing object
302
+ const isLensObject =
303
+ targetNode &&
304
+ typeof targetNode === 'object' &&
305
+ typeof targetNode.get === 'function' &&
306
+ typeof targetNode.set === 'function';
307
+
308
+ if (isLensObject) {
309
+ existed.lens = targetNode;
310
+ existed.node = null;
311
+ } else {
282
312
  existed.node = targetNode;
313
+ existed.lens = null;
283
314
  }
284
- }
285
- if (!existed) {
286
- const lazyCreatedModelItem = FxModel.lazyCreateModelItem(
287
- this.getModel(),
288
- this.ref,
289
- this.nodeset,
290
- this,
291
- );
292
- this.modelItem = lazyCreatedModelItem;
293
- return lazyCreatedModelItem;
315
+
316
+ this.modelItem = existed;
317
+ return existed;
294
318
  }
295
319
  }
296
- this.modelItem = existed;
297
320
 
298
- return existed;
321
+ // 3) Not found: lazily create (lazyCreateModelItem now dedupes/retargets by path)
322
+ const lazyCreatedModelItem = FxModel.lazyCreateModelItem(model, this.ref, effectiveNode, this);
323
+ this.modelItem = lazyCreatedModelItem;
324
+ return lazyCreatedModelItem;
299
325
  }
300
326
  /**
301
327
  * Returns the effective value for the element.