@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/dist/fore-dev.js +4461 -1852
- package/dist/fore.js +4460 -1840
- package/package.json +3 -1
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +471 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/fore.js +28 -71
- package/src/functions/registerFunction.js +65 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +190 -97
- package/src/fx-instance.js +138 -142
- package/src/fx-model.js +292 -102
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-repeat.js +682 -246
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +8 -4
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
|
@@ -82,17 +82,20 @@ export default class FxSetvalue extends AbstractAction {
|
|
|
82
82
|
*/
|
|
83
83
|
dispatchExecute() {}
|
|
84
84
|
|
|
85
|
+
// Adjustment in setValue logic to ensure we work with JSONNode, not just raw values
|
|
85
86
|
setValue(modelItem, newVal) {
|
|
86
87
|
console.log('setValue', modelItem, newVal);
|
|
87
88
|
const item = modelItem;
|
|
88
89
|
if (!item) return;
|
|
89
90
|
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
const path = Fore.getDomNodeIndexString(modelItem.node);
|
|
91
|
+
// Check if current node is a JSONNode
|
|
92
|
+
const node = Array.isArray(item.node) ? item.node[0] : item.node;
|
|
93
93
|
|
|
94
|
+
if (item.value !== newVal) {
|
|
95
|
+
const path = Fore.getDomNodeIndexString(node);
|
|
94
96
|
const ev = this.event;
|
|
95
97
|
const targetElem = this;
|
|
98
|
+
|
|
96
99
|
this.dispatchEvent(
|
|
97
100
|
new CustomEvent('execute-action', {
|
|
98
101
|
composed: true,
|
|
@@ -107,20 +110,19 @@ export default class FxSetvalue extends AbstractAction {
|
|
|
107
110
|
}),
|
|
108
111
|
);
|
|
109
112
|
|
|
113
|
+
// Use ModelItem's value setter which handles both DOM nodes and JSON lenses
|
|
110
114
|
if (newVal?.nodeType) {
|
|
111
115
|
if (newVal.nodeType === Node.ELEMENT_NODE) {
|
|
112
116
|
item.value = newVal;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
if (newVal.nodeType === Node.TEXT_NODE) {
|
|
117
|
+
} else if (newVal.nodeType === Node.ATTRIBUTE_NODE) {
|
|
118
|
+
item.value = newVal.nodeValue;
|
|
119
|
+
} else if (newVal.nodeType === Node.TEXT_NODE) {
|
|
118
120
|
item.value = newVal.textContent;
|
|
119
121
|
}
|
|
120
122
|
} else {
|
|
121
123
|
item.value = newVal;
|
|
122
|
-
item.node.textContent = newVal;
|
|
123
124
|
}
|
|
125
|
+
|
|
124
126
|
this.getModel().changed.push(modelItem);
|
|
125
127
|
this.needsUpdate = true;
|
|
126
128
|
modelItem.notify();
|
package/src/fore.js
CHANGED
|
@@ -293,82 +293,39 @@ export class Fore {
|
|
|
293
293
|
* @returns {Promise<void>}
|
|
294
294
|
*/
|
|
295
295
|
static async refreshChildren(startElement, force) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (children) {
|
|
315
|
-
for (const element of Array.from(children)) {
|
|
316
|
-
if (element.nodeName.toUpperCase() === 'FX-FORE') {
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
|
|
320
|
-
/**
|
|
321
|
-
* @type {import('./ForeElementMixin.js').default}
|
|
322
|
-
*/
|
|
323
|
-
const bound = element;
|
|
324
|
-
if (!force) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
/*
|
|
328
|
-
if(element.nodeName === 'FX-CASE') {
|
|
329
|
-
console.log('hey - got a case', element);
|
|
330
|
-
}
|
|
331
|
-
*/
|
|
332
|
-
if (force === true) {
|
|
333
|
-
// console.log('🔄 refreshing ', element);
|
|
334
|
-
// Unconditional force refresh
|
|
335
|
-
bound.refresh(force);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (typeof force !== 'object') {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
/*
|
|
342
|
-
if (
|
|
343
|
-
force.reason === 'index-function' &&
|
|
344
|
-
bound.dependencies.isInvalidatedByIndexFunction()
|
|
345
|
-
) {
|
|
346
|
-
console.log('🔄 refreshing ', element);
|
|
347
|
-
|
|
348
|
-
bound.refresh(force);
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
bound.dependencies.isInvalidatedByChildlistChanges(force.elementLocalnamesWithChanges)
|
|
354
|
-
) {
|
|
355
|
-
console.log('🔄 refreshing ', element);
|
|
356
|
-
|
|
357
|
-
bound.refresh(force);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
*/
|
|
296
|
+
const children = startElement?.children ? Array.from(startElement.children) : [];
|
|
297
|
+
for (const element of children) {
|
|
298
|
+
// Do not cross into nested fore roots
|
|
299
|
+
if (element.nodeName.toUpperCase() === 'FX-FORE') {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
|
|
304
|
+
/** @type {import('./ForeElementMixin.js').default} */
|
|
305
|
+
const bound = element;
|
|
306
|
+
|
|
307
|
+
// Keep old behavior: only refresh UI elements during full/forced refresh
|
|
308
|
+
if (!force) {
|
|
309
|
+
// still recurse below
|
|
310
|
+
} else if (force === true) {
|
|
311
|
+
const maybePromise = bound.refresh(force);
|
|
312
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
313
|
+
await maybePromise;
|
|
361
314
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
315
|
+
} else if (typeof force === 'object') {
|
|
316
|
+
// future selective refresh logic can live here if you re-enable it
|
|
317
|
+
const maybePromise = bound.refresh(force);
|
|
318
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
319
|
+
await maybePromise;
|
|
365
320
|
}
|
|
366
321
|
}
|
|
367
322
|
}
|
|
368
|
-
resolve('done');
|
|
369
|
-
});
|
|
370
323
|
|
|
371
|
-
|
|
324
|
+
// Traverse DOM unless inert
|
|
325
|
+
if (!(element.inert === true)) {
|
|
326
|
+
await Fore.refreshChildren(element, force);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
372
329
|
}
|
|
373
330
|
|
|
374
331
|
static copyDom(inputElement) {
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { createTypedValueFactory, registerCustomXPathFunction } from 'fontoxpath';
|
|
2
2
|
import { evaluateXPath, globallyDeclaredFunctionLocalNames } from '../xpath-evaluation.js';
|
|
3
3
|
|
|
4
|
+
// FontoXPath custom functions are registered globally. If multiple <fx-functionlib> (or multiple
|
|
5
|
+
// <fx-fore> instances) register the same function name+arity, later registrations would overwrite
|
|
6
|
+
// earlier ones across the whole page. We enforce "first wins".
|
|
7
|
+
const _registeredFunctionKeys = new Set();
|
|
8
|
+
|
|
9
|
+
function _makeFunctionKey(functionIdentifier, arity) {
|
|
10
|
+
if (typeof functionIdentifier === 'string') {
|
|
11
|
+
return `str:${functionIdentifier}#${arity}`;
|
|
12
|
+
}
|
|
13
|
+
return `{${functionIdentifier.namespaceURI}}${functionIdentifier.localName}#${arity}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function _ensureGlobalUnprefixedName(localName) {
|
|
17
|
+
// Keep this list unique to avoid unbounded growth across tests
|
|
18
|
+
if (!globallyDeclaredFunctionLocalNames.includes(localName)) {
|
|
19
|
+
globallyDeclaredFunctionLocalNames.push(localName);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
|
-
* @param functionObject {{signature: string, type: string|null, functionBody: string}}
|
|
24
|
+
* @param functionObject {{signature: string, type: string|null, functionBody: string, implementation?: Function}}
|
|
6
25
|
* @param formElement {HTMLElement} The form element connected to this function. Used to determine inscope context
|
|
7
26
|
* @returns {undefined}
|
|
8
27
|
*/
|
|
@@ -31,29 +50,51 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
31
50
|
? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
|
|
32
51
|
: `${prefix}:${localName}`;
|
|
33
52
|
|
|
34
|
-
// Make the function available globally w/o a prefix. See the functionNameResolver for for how
|
|
35
|
-
// functionObject is picked up
|
|
36
|
-
if (!prefix) {
|
|
37
|
-
globallyDeclaredFunctionLocalNames.push(localName);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
53
|
const paramParts = params
|
|
41
|
-
? params
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
? params
|
|
55
|
+
.split(',')
|
|
56
|
+
.map(param => param.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map(param => {
|
|
59
|
+
const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>[^\s]+))/);
|
|
60
|
+
if (!match) {
|
|
61
|
+
throw new Error(`Param ${param} could not be parsed`);
|
|
62
|
+
}
|
|
63
|
+
const { variableName, varType } = match.groups;
|
|
64
|
+
return {
|
|
65
|
+
variableName,
|
|
66
|
+
variableType: varType || 'item()*',
|
|
67
|
+
};
|
|
68
|
+
})
|
|
52
69
|
: [];
|
|
53
70
|
|
|
71
|
+
const arity = paramParts.length;
|
|
72
|
+
|
|
73
|
+
// -------------------------------------------------
|
|
74
|
+
// FIRST-WINS GUARD (name + arity) WITH NAME EXPORT
|
|
75
|
+
// -------------------------------------------------
|
|
76
|
+
const key = _makeFunctionKey(functionIdentifier, arity);
|
|
77
|
+
|
|
78
|
+
if (_registeredFunctionKeys.has(key)) {
|
|
79
|
+
// If this registration is unprefixed, we must still make it callable without prefix.
|
|
80
|
+
// This fixes the unit test case where local:hello-world() was registered earlier and
|
|
81
|
+
// hello-world() should resolve to that implementation.
|
|
82
|
+
if (!prefix) {
|
|
83
|
+
_ensureGlobalUnprefixedName(localName);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_registeredFunctionKeys.add(key);
|
|
89
|
+
|
|
90
|
+
// Make the function available globally w/o a prefix.
|
|
91
|
+
if (!prefix) {
|
|
92
|
+
_ensureGlobalUnprefixedName(localName);
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
switch (type) {
|
|
55
96
|
case 'text/javascript': {
|
|
56
|
-
//
|
|
97
|
+
// If a real JS function is provided (module libs), register it directly.
|
|
57
98
|
if (typeof functionObject.implementation === 'function') {
|
|
58
99
|
const impl = functionObject.implementation;
|
|
59
100
|
registerCustomXPathFunction(
|
|
@@ -74,6 +115,7 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
74
115
|
'form',
|
|
75
116
|
functionObject.functionBody,
|
|
76
117
|
);
|
|
118
|
+
|
|
77
119
|
registerCustomXPathFunction(
|
|
78
120
|
functionIdentifier,
|
|
79
121
|
paramParts.map(paramPart => paramPart.variableType),
|
|
@@ -83,25 +125,27 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
83
125
|
);
|
|
84
126
|
break;
|
|
85
127
|
}
|
|
128
|
+
|
|
86
129
|
case 'text/xquf':
|
|
87
130
|
case 'text/xquery':
|
|
88
131
|
case 'text/xpath': {
|
|
89
132
|
const typedValueFactories = paramParts.map(param =>
|
|
90
133
|
createTypedValueFactory(param.variableType),
|
|
91
134
|
);
|
|
135
|
+
|
|
92
136
|
const language =
|
|
93
137
|
type === 'text/xpath'
|
|
94
138
|
? 'XPath3.1'
|
|
95
139
|
: type === 'text/xquery'
|
|
96
140
|
? 'XQuery3.1'
|
|
97
141
|
: 'XQueryUpdate3.1';
|
|
142
|
+
|
|
98
143
|
const fun = (domFacade, ...args) =>
|
|
99
144
|
evaluateXPath(
|
|
100
145
|
functionObject.functionBody,
|
|
101
146
|
formElement.getInScopeContext(),
|
|
102
147
|
formElement.getOwnerForm(),
|
|
103
148
|
paramParts.reduce((variablesByName, paramPart, i) => {
|
|
104
|
-
// Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
|
|
105
149
|
variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](
|
|
106
150
|
args[i],
|
|
107
151
|
domFacade,
|
|
@@ -110,6 +154,7 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
110
154
|
}, {}),
|
|
111
155
|
{ language },
|
|
112
156
|
);
|
|
157
|
+
|
|
113
158
|
registerCustomXPathFunction(
|
|
114
159
|
functionIdentifier,
|
|
115
160
|
paramParts.map(paramPart => paramPart.variableType),
|
package/src/fx-bind.js
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import { XPathUtil } from './xpath-util.js';
|
|
10
10
|
import getInScopeContext from './getInScopeContext.js';
|
|
11
11
|
import { getPath } from './xpath-path.js';
|
|
12
|
-
import { evaluateXPathToFirstNode } from 'fontoxpath';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* FxBind declaratively attaches constraints to nodes in the data (instances).
|
|
@@ -157,86 +156,75 @@ export class FxBind extends ForeElementMixin {
|
|
|
157
156
|
*/
|
|
158
157
|
init(model) {
|
|
159
158
|
this.model = model;
|
|
160
|
-
// console.log('init binding ', this);
|
|
161
159
|
this._getInstanceId();
|
|
162
160
|
this.bindType = this.getModel().getInstance(this.instanceId).type;
|
|
163
|
-
// console.log('binding type ', this.bindType);
|
|
164
161
|
|
|
162
|
+
// ✅ Always evaluate nodeset first (XML + JSON)
|
|
163
|
+
this._evalInContext();
|
|
164
|
+
|
|
165
|
+
// ✅ Build dependency graph for both types
|
|
166
|
+
this._buildBindGraph();
|
|
167
|
+
|
|
168
|
+
// ✅ Create modelitems for both types
|
|
165
169
|
if (this.bindType === 'xml') {
|
|
166
|
-
this._evalInContext();
|
|
167
|
-
this._buildBindGraph();
|
|
168
170
|
this._createModelItems();
|
|
171
|
+
} else if (this.bindType === 'json') {
|
|
172
|
+
this._createModelItemsForJSON();
|
|
169
173
|
}
|
|
170
|
-
// todo: support json
|
|
171
174
|
|
|
172
|
-
// ### process child bindings
|
|
173
175
|
this._processChildren(model);
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
_buildBindGraph() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const path = getPath(node, instance);
|
|
182
|
-
this.model.mainGraph.addNode(path, node);
|
|
183
|
-
|
|
184
|
-
/* ### catching references in the 'ref' itself...
|
|
185
|
-
todo: investigate cases where 'ref' attributes use predicates pointing to other nodes. These would not be handled
|
|
186
|
-
in current implementation.
|
|
187
|
-
|
|
188
|
-
General question: are there valid use-cases for using a 'filter' expression to narrow the nodeset
|
|
189
|
-
where to apply constraints? Guess yes and if it's 'just' for reducing the amount of necessary modelItem objects.
|
|
179
|
+
// ✅ Works for XML and JSON (JSON nodes have getPath()/getPath() handles __jsonlens__)
|
|
180
|
+
this.nodeset.forEach(node => {
|
|
181
|
+
const instanceId = XPathUtil.resolveInstance(this, this.ref);
|
|
182
|
+
const path = getPath(node, instanceId);
|
|
190
183
|
|
|
191
|
-
|
|
192
|
-
// const foreignRefs = this.getReferences(this.ref);
|
|
184
|
+
this.model.mainGraph.addNode(path, node);
|
|
193
185
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
186
|
+
if (this.calculate) {
|
|
187
|
+
this.model.mainGraph.addNode(`${path}:calculate`, node);
|
|
188
|
+
this.model.mainGraph.addDependency(path, `${path}:calculate`);
|
|
189
|
+
}
|
|
199
190
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
191
|
+
const calculateRefs = this._getReferencesForProperty(this.calculate, node);
|
|
192
|
+
if (calculateRefs.length !== 0) {
|
|
193
|
+
this._addDependencies(calculateRefs, node, path, 'calculate', instanceId);
|
|
194
|
+
}
|
|
204
195
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
196
|
+
if (!this.calculate) {
|
|
197
|
+
const readonlyRefs = this._getReferencesForProperty(this.readonly, node);
|
|
198
|
+
if (readonlyRefs.length !== 0) {
|
|
199
|
+
this._addDependencies(readonlyRefs, node, path, 'readonly', instanceId);
|
|
200
|
+
} else if (this.readonly) {
|
|
201
|
+
this.model.mainGraph.addNode(`${path}:readonly`, node);
|
|
212
202
|
}
|
|
203
|
+
}
|
|
213
204
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
205
|
+
const requiredRefs = this._getReferencesForProperty(this.required, node);
|
|
206
|
+
if (requiredRefs.length !== 0) {
|
|
207
|
+
this._addDependencies(requiredRefs, node, path, 'required', instanceId);
|
|
208
|
+
} else if (this.required) {
|
|
209
|
+
this.model.mainGraph.addNode(`${path}:required`, node);
|
|
210
|
+
}
|
|
221
211
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
212
|
+
const relevantRefs = this._getReferencesForProperty(this.relevant, node);
|
|
213
|
+
if (relevantRefs.length !== 0) {
|
|
214
|
+
this._addDependencies(relevantRefs, node, path, 'relevant', instanceId);
|
|
215
|
+
} else if (this.relevant) {
|
|
216
|
+
this.model.mainGraph.addNode(`${path}:relevant`, node);
|
|
217
|
+
}
|
|
228
218
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
219
|
+
const constraintRefs = this._getReferencesForProperty(this.constraint, node);
|
|
220
|
+
if (constraintRefs.length !== 0) {
|
|
221
|
+
this._addDependencies(constraintRefs, node, path, 'constraint', instanceId);
|
|
222
|
+
} else if (this.constraint) {
|
|
223
|
+
this.model.mainGraph.addNode(`${path}:constraint`, node);
|
|
224
|
+
this.model.mainGraph.addDependency(path, `${path}:constraint`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
238
227
|
}
|
|
239
|
-
|
|
240
228
|
/**
|
|
241
229
|
* Resolves a referenced ModelItem using the model's graph and node registry.
|
|
242
230
|
* @param {string} refName
|
|
@@ -266,26 +254,25 @@ export class FxBind extends ForeElementMixin {
|
|
|
266
254
|
* @param {string} path The path to the start of the reference
|
|
267
255
|
* @param {string} property The property with this dependency
|
|
268
256
|
*/
|
|
269
|
-
_addDependencies(refs, node, path, property) {
|
|
270
|
-
// console.log('_addDependencies',path);
|
|
257
|
+
_addDependencies(refs, node, path, property, instanceId) {
|
|
271
258
|
const nodeHash = `${path}:${property}`;
|
|
259
|
+
|
|
272
260
|
if (refs.length !== 0) {
|
|
273
261
|
if (!this.model.mainGraph.hasNode(nodeHash)) {
|
|
274
262
|
this.model.mainGraph.addNode(nodeHash, node);
|
|
275
263
|
}
|
|
264
|
+
|
|
276
265
|
refs.forEach(ref => {
|
|
277
|
-
const
|
|
266
|
+
const otherPath = getPath(ref, instanceId);
|
|
278
267
|
|
|
279
|
-
|
|
280
|
-
|
|
268
|
+
// keep old XML-only hack
|
|
269
|
+
if (this.bindType === 'xml' && otherPath.endsWith('text()[1]')) return;
|
|
281
270
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (!this.model.mainGraph.hasNode(otherPath)) {
|
|
285
|
-
this.model.mainGraph.addNode(otherPath, ref);
|
|
286
|
-
}
|
|
287
|
-
this.model.mainGraph.addDependency(nodeHash, otherPath);
|
|
271
|
+
if (!this.model.mainGraph.hasNode(otherPath)) {
|
|
272
|
+
this.model.mainGraph.addNode(otherPath, ref);
|
|
288
273
|
}
|
|
274
|
+
|
|
275
|
+
this.model.mainGraph.addDependency(nodeHash, otherPath);
|
|
289
276
|
});
|
|
290
277
|
} else {
|
|
291
278
|
this.model.mainGraph.addNode(nodeHash, node);
|
|
@@ -311,6 +298,23 @@ export class FxBind extends ForeElementMixin {
|
|
|
311
298
|
return null;
|
|
312
299
|
}
|
|
313
300
|
|
|
301
|
+
_createModelItemsForJSON() {
|
|
302
|
+
const fore = this.closest('fx-fore');
|
|
303
|
+
const instanceId = this.instanceId;
|
|
304
|
+
|
|
305
|
+
this.nodeset.forEach(jsonNode => {
|
|
306
|
+
const path = getPath(jsonNode, instanceId);
|
|
307
|
+
|
|
308
|
+
// ✅ ModelItem node should be the JSONNode itself (lens), NOT JSONLens
|
|
309
|
+
const newItem = new ModelItem(path, this.getBindingExpr(), jsonNode, this, instanceId, fore);
|
|
310
|
+
|
|
311
|
+
const alert = this.getAlert();
|
|
312
|
+
if (alert) newItem.addAlert(alert);
|
|
313
|
+
|
|
314
|
+
this.getModel().registerModelItem(newItem);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
314
318
|
/**
|
|
315
319
|
* overwrites
|
|
316
320
|
*/
|
|
@@ -346,8 +350,11 @@ export class FxBind extends ForeElementMixin {
|
|
|
346
350
|
const inst = this.getModel().getInstance(this.instanceId);
|
|
347
351
|
if (inst.type === 'xml') {
|
|
348
352
|
this.nodeset = evaluateXPathToNodes(this.ref, inscopeContext, this);
|
|
353
|
+
} else if (inst.type === 'json') {
|
|
354
|
+
// ✅ JSON must also resolve the nodeset via XPath evaluation
|
|
355
|
+
this.nodeset = evaluateXPathToNodes(this.ref, inscopeContext, this);
|
|
349
356
|
} else {
|
|
350
|
-
this.nodeset =
|
|
357
|
+
this.nodeset = [];
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
}
|
|
@@ -458,6 +465,33 @@ export class FxBind extends ForeElementMixin {
|
|
|
458
465
|
}
|
|
459
466
|
|
|
460
467
|
getReferences(propertyExpr) {
|
|
468
|
+
// For XML, DependencyNotifyingDomFacade reliably reports the nodes touched during evaluation.
|
|
469
|
+
// For JSON lens nodes, the domFacade hook does not fire (evaluation goes through our lens resolver),
|
|
470
|
+
// so we must extract lookup tokens and resolve them explicitly.
|
|
471
|
+
|
|
472
|
+
if (!propertyExpr) return [];
|
|
473
|
+
|
|
474
|
+
// JSON path: resolve dependencies by parsing lens lookups in the expression.
|
|
475
|
+
if (this.bindType === 'json') {
|
|
476
|
+
const touchedNodes = new Set();
|
|
477
|
+
const tokens = this._extractJsonLookupTokens(propertyExpr);
|
|
478
|
+
|
|
479
|
+
// Evaluate each token in the *current* context node (each item in nodeset)
|
|
480
|
+
this.nodeset.forEach(node => {
|
|
481
|
+
tokens.forEach(token => {
|
|
482
|
+
try {
|
|
483
|
+
const refs = evaluateXPathToNodes(token, node, this);
|
|
484
|
+
refs.forEach(r => touchedNodes.add(r));
|
|
485
|
+
} catch (_e) {
|
|
486
|
+
// ignore: dependency extraction must never break bind initialization
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return Array.from(touchedNodes.values());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// XML path: use dom facade for accurate dependency tracking
|
|
461
495
|
const touchedNodes = new Set();
|
|
462
496
|
const domFacade = new DependencyNotifyingDomFacade(otherNode => touchedNodes.add(otherNode));
|
|
463
497
|
this.nodeset.forEach(node => {
|
|
@@ -465,6 +499,27 @@ export class FxBind extends ForeElementMixin {
|
|
|
465
499
|
});
|
|
466
500
|
return Array.from(touchedNodes.values());
|
|
467
501
|
}
|
|
502
|
+
_extractJsonLookupTokens(expr) {
|
|
503
|
+
if (!expr) return [];
|
|
504
|
+
|
|
505
|
+
const src = String(expr);
|
|
506
|
+
const tokens = new Set();
|
|
507
|
+
|
|
508
|
+
// instance('id')?a?b?c or instance('id')?*
|
|
509
|
+
const instRe = /instance\s*\([^)]*\)\s*(?:\?\s*\*|\?\s*[a-zA-Z_][\w-]*)+/g;
|
|
510
|
+
let m;
|
|
511
|
+
while ((m = instRe.exec(src)) !== null) {
|
|
512
|
+
if (m[0]) tokens.add(m[0].replace(/\s+/g, ''));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// relative lookups like ?title, ?year, ?ui, ?query (ignore ?*)
|
|
516
|
+
const relRe = /\?[a-zA-Z_][\w-]*/g;
|
|
517
|
+
while ((m = relRe.exec(src)) !== null) {
|
|
518
|
+
if (m[0] && m[0] !== '?*') tokens.add(m[0]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return Array.from(tokens);
|
|
522
|
+
}
|
|
468
523
|
|
|
469
524
|
/*
|
|
470
525
|
static getReferencesForRef(ref,nodeset){
|