@jinntec/fore 3.3.2 → 4.0.0
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 +98 -70
- package/dist/fore-dev.js +5593 -6832
- package/dist/fore.js +5602 -4887
- package/index.js +5 -10
- package/package.json +7 -7
- package/resources/fore.css +33 -0
- package/src/DependencyNotifyingDomFacade.js +90 -21
- package/src/DependentXPathQueries.js +15 -2
- package/src/ForeElementMixin.js +110 -16
- package/src/UndoManager.js +267 -0
- package/src/actions/abstract-action.js +71 -30
- package/src/actions/fx-action.js +5 -0
- package/src/actions/fx-append.js +3 -3
- package/src/actions/fx-commit-history.js +26 -0
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +25 -22
- package/src/actions/fx-load.js +5 -5
- package/src/actions/fx-redo.js +58 -0
- package/src/actions/fx-refresh.js +2 -2
- package/src/actions/fx-reload.js +1 -1
- package/src/actions/fx-replace.js +1 -1
- package/src/actions/fx-send.js +27 -5
- package/src/actions/fx-setattribute.js +11 -7
- package/src/actions/fx-undo.js +58 -0
- package/src/createNodes.js +314 -0
- package/src/fore.js +53 -18
- package/src/functions/fx-functionlib.js +10 -10
- package/src/fx-bind.js +30 -18
- package/src/fx-fore.js +222 -200
- package/src/fx-instance.js +18 -1
- package/src/fx-model.js +236 -69
- package/src/fx-submission.js +37 -29
- package/src/fx-var.js +49 -13
- package/src/getInScopeContext.js +1 -1
- package/src/json/JSONDomFacade.js +1 -1
- package/src/json/JSONLens.js +2 -2
- package/src/ui/UIElement.js +18 -8
- package/src/ui/abstract-control.js +45 -3
- package/src/ui/fx-alert.js +4 -0
- package/src/ui/fx-case.js +1 -1
- package/src/ui/fx-container.js +3 -0
- package/src/ui/fx-control-menu.js +79 -11
- package/src/ui/fx-control.js +130 -41
- package/src/ui/fx-dialog.js +5 -0
- package/src/ui/fx-items.js +6 -6
- package/src/ui/fx-output.js +37 -1
- package/src/ui/fx-repeat.js +1065 -103
- package/src/ui/fx-repeatitem.js +4 -1
- package/src/ui/fx-switch.js +116 -3
- package/src/ui/fx-trigger.js +9 -4
- package/src/ui/fx-upload.js +10 -4
- package/src/ui/repeat-base.js +20 -12
- package/src/withDraggability.js +10 -1
- package/src/xpath-evaluation.js +30 -18
- package/src/xpath-path.js +122 -0
- package/src/xpath-util.js +11 -126
- package/src/actions/StringTpl.js +0 -17
- package/src/extract-predicate-deps.js +0 -57
- package/src/extractPredicateDependencies.js +0 -36
- package/src/json/lensFromNode.js +0 -5
- package/src/json-util.js +0 -27
- package/src/tools/adi.js +0 -1111
- package/src/tools/deprecation.md +0 -1
- package/src/tools/fx-action-log.js +0 -745
- package/src/tools/fx-devtools.js +0 -444
- package/src/tools/fx-dom-inspector.js +0 -610
- package/src/tools/fx-json-instance.js +0 -444
- package/src/tools/fx-log-item.js +0 -128
- package/src/tools/fx-log-settings.js +0 -533
- package/src/tools/fx-minimap.js +0 -203
- package/src/tools/helpers.js +0 -132
- package/src/ui/TemplateExpression.js +0 -12
- package/src/ui/fx-dom-inspector.js +0 -1255
package/index.js
CHANGED
|
@@ -27,24 +27,16 @@ import './src/ui/fx-dialog.js';
|
|
|
27
27
|
import './src/ui/fx-items.js';
|
|
28
28
|
import './src/ui/fx-droptarget.js';
|
|
29
29
|
|
|
30
|
-
import './src/tools/fx-action-log.js';
|
|
31
|
-
import './src/tools/fx-log-settings.js';
|
|
32
|
-
import './src/tools/fx-log-item.js';
|
|
33
|
-
import './src/tools/fx-devtools.js';
|
|
34
|
-
import './src/tools/fx-dom-inspector.js';
|
|
35
30
|
import './src/lab/fore-component.js';
|
|
36
|
-
import './src/tools/fx-json-instance.js';
|
|
37
31
|
import './src/ui/fx-upload.js';
|
|
38
|
-
// import './src/tools/fx-minimap.js';
|
|
39
|
-
|
|
40
|
-
// import './src/ui/fx-checkbox-group.js';
|
|
41
32
|
|
|
42
33
|
// action classes
|
|
43
34
|
import './src/actions/fx-append.js';
|
|
44
35
|
import './src/actions/fx-delete.js';
|
|
45
36
|
import './src/actions/fx-setfocus.js';
|
|
46
37
|
import './src/actions/fx-insert.js';
|
|
47
|
-
|
|
38
|
+
import './src/actions/fx-insertchild.js';
|
|
39
|
+
import './src/actions/fx-copy.js';
|
|
48
40
|
import './src/actions/fx-message.js';
|
|
49
41
|
import './src/actions/fx-setvalue.js';
|
|
50
42
|
import './src/actions/fx-send.js';
|
|
@@ -59,6 +51,9 @@ import './src/actions/fx-show.js';
|
|
|
59
51
|
import './src/actions/fx-hide.js';
|
|
60
52
|
import './src/actions/fx-reload.js';
|
|
61
53
|
import './src/actions/fx-reset.js';
|
|
54
|
+
import './src/actions/fx-undo.js';
|
|
55
|
+
import './src/actions/fx-redo.js';
|
|
56
|
+
import './src/actions/fx-commit-history.js';
|
|
62
57
|
import './src/actions/fx-load.js';
|
|
63
58
|
import './src/actions/fx-toggleboolean.js';
|
|
64
59
|
import './src/actions/fx-call.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinntec/fore",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Fore - declarative user interfaces in plain HTML",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@jinntec/jinn-toast": "^1.0.5",
|
|
34
34
|
"@picocss/pico": "^2.0.6",
|
|
35
|
-
"chai": "^5.2.1",
|
|
36
35
|
"fontoxpath": "^3.34.0"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
@@ -40,10 +39,9 @@
|
|
|
40
39
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
|
41
40
|
"@babel/plugin-transform-private-methods": "^7.28.6",
|
|
42
41
|
"@jinntec/jinn-codemirror": "^1.17.6",
|
|
43
|
-
"@open-wc/building-rollup": "^2.0.1",
|
|
44
42
|
"@open-wc/eslint-config": "^7.0.0",
|
|
45
43
|
"@open-wc/prettier-config": "^0.1.10",
|
|
46
|
-
"@open-wc/semantic-dom-diff": "^0.
|
|
44
|
+
"@open-wc/semantic-dom-diff": "^0.20.1",
|
|
47
45
|
"@open-wc/testing": "^3.1.5",
|
|
48
46
|
"@open-wc/testing-karma": "^4.0.9",
|
|
49
47
|
"@open-wc/testing-karma-bs": "^1.3.94",
|
|
@@ -54,16 +52,18 @@
|
|
|
54
52
|
"@skypack/package-check": "^0.2.2",
|
|
55
53
|
"@types/cypress": "^1.1.3",
|
|
56
54
|
"@webcomponents/webcomponentsjs": "^2.6.0",
|
|
57
|
-
"
|
|
55
|
+
"axe-core": "^4.12.1",
|
|
56
|
+
"chai": "^5.2.1",
|
|
57
|
+
"cypress": "^15.18.0",
|
|
58
|
+
"cypress-axe": "^1.7.0",
|
|
58
59
|
"deepmerge": "^4.2.2",
|
|
59
60
|
"eslint": "^8.16.0",
|
|
60
61
|
"eslint-plugin-prettier": "^5.1.3",
|
|
61
62
|
"husky": "^8.0.1",
|
|
62
|
-
"jsdoc": "^
|
|
63
|
+
"jsdoc": "^4.0.5",
|
|
63
64
|
"lint-staged": "^12.4.3",
|
|
64
65
|
"lit-html": "^2.2.5",
|
|
65
66
|
"mocha": "^10.0.0",
|
|
66
|
-
"np": "^8.0.4",
|
|
67
67
|
"replace-in-file": "^8.0.2",
|
|
68
68
|
"rimraf": "^3.0.2",
|
|
69
69
|
"rollup": "^2.75.4",
|
package/resources/fore.css
CHANGED
|
@@ -214,6 +214,39 @@
|
|
|
214
214
|
position: relative;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/* ### NATIVE <dialog> STYLES (preferred over fx-dialog, which is deprecated) ### */
|
|
218
|
+
/* ### NATIVE <dialog> STYLES (preferred over fx-dialog, which is deprecated) ### */
|
|
219
|
+
/* ### NATIVE <dialog> STYLES (preferred over fx-dialog, which is deprecated) ### */
|
|
220
|
+
dialog {
|
|
221
|
+
border: thin solid;
|
|
222
|
+
border-radius: 0.5rem;
|
|
223
|
+
padding: 0;
|
|
224
|
+
position: relative;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
dialog::backdrop {
|
|
228
|
+
background: rgba(0, 0, 0, 0.5);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
dialog .dialog-content {
|
|
232
|
+
padding: 1rem;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
dialog .close-dialog {
|
|
236
|
+
position: absolute;
|
|
237
|
+
right: 0.4rem;
|
|
238
|
+
top: 0.2rem;
|
|
239
|
+
color: var(--paper-grey-900);
|
|
240
|
+
text-decoration: none;
|
|
241
|
+
font-size: 1.2rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
dialog .action {
|
|
245
|
+
width: 100%;
|
|
246
|
+
text-align: center;
|
|
247
|
+
display: block;
|
|
248
|
+
}
|
|
249
|
+
|
|
217
250
|
fx-hint {
|
|
218
251
|
display: none;
|
|
219
252
|
}
|
|
@@ -5,15 +5,66 @@ import { getBucketsForNode } from 'fontoxpath';
|
|
|
5
5
|
* as the `depends` function, but less explicit and will automatically be called for any node that
|
|
6
6
|
* will be touched in the XPath.
|
|
7
7
|
*
|
|
8
|
+
* By default it reads from the live DOM. When an `inner` facade is provided (e.g. the
|
|
9
|
+
* JSONDomFacade), all accessors delegate to it instead, still notifying `onNodeTouched` —
|
|
10
|
+
* this lets non-DOM data models participate in dependency tracking.
|
|
11
|
+
*
|
|
8
12
|
* Maybe some more granularity is better. Maybe only notify a node's attributes are touched?
|
|
9
13
|
*
|
|
10
14
|
*/
|
|
11
15
|
export class DependencyNotifyingDomFacade {
|
|
12
16
|
/**
|
|
13
17
|
* @param onNodeTouched - onNodeTouched A function what will be executed whenever a node is 'touched' by the XPath
|
|
18
|
+
* @param inner - optional facade to delegate all accessors to (e.g. JSONDomFacade)
|
|
14
19
|
*/
|
|
15
|
-
constructor(onNodeTouched) {
|
|
20
|
+
constructor(onNodeTouched, inner = null) {
|
|
16
21
|
this._onNodeTouched = onNodeTouched;
|
|
22
|
+
this._inner = inner;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns a facade sharing the same onNodeTouched callback but delegating to `inner`.
|
|
27
|
+
* Used by xpath-evaluation.js to route JSON contexts through the JSONDomFacade
|
|
28
|
+
* without callers having to know the context type.
|
|
29
|
+
*
|
|
30
|
+
* @param inner - the facade to delegate to
|
|
31
|
+
*/
|
|
32
|
+
withInner(inner) {
|
|
33
|
+
return new DependencyNotifyingDomFacade(this._onNodeTouched, inner);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Node identity/shape reads (used by the JSONDomFacade surface). Not treated as
|
|
38
|
+
* data dependencies — no notification.
|
|
39
|
+
*/
|
|
40
|
+
getNodeType(node) {
|
|
41
|
+
if (this._inner) return this._inner.getNodeType(node);
|
|
42
|
+
return node.nodeType;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getNodeName(node) {
|
|
46
|
+
if (this._inner) return this._inner.getNodeName(node);
|
|
47
|
+
return node.nodeName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getNodeValue(node) {
|
|
51
|
+
this._onNodeTouched(node);
|
|
52
|
+
if (this._inner) return this._inner.getNodeValue(node);
|
|
53
|
+
return node.nodeValue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getChildren(node) {
|
|
57
|
+
if (this._inner) {
|
|
58
|
+
const children = this._inner.getChildren(node);
|
|
59
|
+
children.forEach(child => this._onNodeTouched(child));
|
|
60
|
+
return children;
|
|
61
|
+
}
|
|
62
|
+
return this.getChildNodes(node);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getChildNodeCount(node) {
|
|
66
|
+
if (this._inner) return this._inner.getChildNodeCount(node);
|
|
67
|
+
return node.childNodes.length;
|
|
17
68
|
}
|
|
18
69
|
|
|
19
70
|
/**
|
|
@@ -22,9 +73,9 @@ export class DependencyNotifyingDomFacade {
|
|
|
22
73
|
*
|
|
23
74
|
* @param node -
|
|
24
75
|
*/
|
|
25
|
-
// eslint-disable-next-line class-methods-use-this
|
|
26
76
|
getAllAttributes(node) {
|
|
27
77
|
this._onNodeTouched(node); // <== Important!
|
|
78
|
+
if (this._inner) return this._inner.getAllAttributes(node);
|
|
28
79
|
return Array.from(node.attributes);
|
|
29
80
|
}
|
|
30
81
|
|
|
@@ -34,10 +85,12 @@ export class DependencyNotifyingDomFacade {
|
|
|
34
85
|
* @param node -
|
|
35
86
|
* @param attributeName -
|
|
36
87
|
*/
|
|
37
|
-
// eslint-disable-next-line class-methods-use-this
|
|
38
88
|
getAttribute(node, attributeName) {
|
|
89
|
+
if (this._inner) {
|
|
90
|
+
this._onNodeTouched(node);
|
|
91
|
+
return this._inner.getAttribute(node, attributeName);
|
|
92
|
+
}
|
|
39
93
|
const attr = node.getAttributeNode(attributeName);
|
|
40
|
-
console.log('[DomFacade] getAttribute touched:', attr);
|
|
41
94
|
if (attr) this._onNodeTouched(attr);
|
|
42
95
|
return attr?.value ?? null;
|
|
43
96
|
}
|
|
@@ -49,8 +102,12 @@ export class DependencyNotifyingDomFacade {
|
|
|
49
102
|
* @param node -
|
|
50
103
|
* @param bucket - The bucket that matches the attribute that will be used.
|
|
51
104
|
*/
|
|
52
|
-
// eslint-disable-next-line class-methods-use-this
|
|
53
105
|
getChildNodes(node, bucket) {
|
|
106
|
+
if (this._inner) {
|
|
107
|
+
const children = this._inner.getChildNodes(node, bucket);
|
|
108
|
+
children.forEach(child => this._onNodeTouched(child));
|
|
109
|
+
return children;
|
|
110
|
+
}
|
|
54
111
|
const matchingNodes = Array.from(node.childNodes).filter(
|
|
55
112
|
childNode => !bucket || getBucketsForNode(childNode).includes(bucket),
|
|
56
113
|
);
|
|
@@ -64,13 +121,15 @@ export class DependencyNotifyingDomFacade {
|
|
|
64
121
|
* @param node -
|
|
65
122
|
*/
|
|
66
123
|
getData(node) {
|
|
124
|
+
if (this._inner) {
|
|
125
|
+
this._onNodeTouched(node);
|
|
126
|
+
return this._inner.getData(node);
|
|
127
|
+
}
|
|
67
128
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
68
|
-
// console.log('[DomFacade] getData on attribute:', node);
|
|
69
129
|
this._onNodeTouched(node);
|
|
70
130
|
return node.value;
|
|
71
131
|
}
|
|
72
132
|
// Text node
|
|
73
|
-
// console.log('[DomFacade] getData on text node parent:', node.parentNode);
|
|
74
133
|
this._onNodeTouched(node.parentNode);
|
|
75
134
|
return node.data;
|
|
76
135
|
}
|
|
@@ -83,6 +142,11 @@ export class DependencyNotifyingDomFacade {
|
|
|
83
142
|
* @param bucket - The bucket that matches the attribute that will be used.
|
|
84
143
|
*/
|
|
85
144
|
getFirstChild(node, bucket) {
|
|
145
|
+
if (this._inner) {
|
|
146
|
+
const child = this._inner.getFirstChild(node, bucket);
|
|
147
|
+
if (child) this._onNodeTouched(child);
|
|
148
|
+
return child;
|
|
149
|
+
}
|
|
86
150
|
for (const child of node.childNodes) {
|
|
87
151
|
if (!bucket || getBucketsForNode(child).includes(bucket)) {
|
|
88
152
|
this._onNodeTouched(child);
|
|
@@ -99,8 +163,12 @@ export class DependencyNotifyingDomFacade {
|
|
|
99
163
|
* @param node -
|
|
100
164
|
* @param bucket - The bucket that matches the attribute that will be used.
|
|
101
165
|
*/
|
|
102
|
-
// eslint-disable-next-line class-methods-use-this
|
|
103
166
|
getLastChild(node, bucket) {
|
|
167
|
+
if (this._inner) {
|
|
168
|
+
const child = this._inner.getLastChild(node, bucket);
|
|
169
|
+
if (child) this._onNodeTouched(child);
|
|
170
|
+
return child;
|
|
171
|
+
}
|
|
104
172
|
const children = Array.from(node.childNodes).filter(
|
|
105
173
|
child => !bucket || getBucketsForNode(child).includes(bucket),
|
|
106
174
|
);
|
|
@@ -116,8 +184,12 @@ export class DependencyNotifyingDomFacade {
|
|
|
116
184
|
* @param node -
|
|
117
185
|
* @param bucket - The bucket that matches the nextSibling that is requested.
|
|
118
186
|
*/
|
|
119
|
-
// eslint-disable-next-line class-methods-use-this
|
|
120
187
|
getNextSibling(node, bucket) {
|
|
188
|
+
if (this._inner) {
|
|
189
|
+
const sibling = this._inner.getNextSibling(node, bucket);
|
|
190
|
+
if (sibling) this._onNodeTouched(sibling);
|
|
191
|
+
return sibling;
|
|
192
|
+
}
|
|
121
193
|
for (let sibling = node.nextSibling; sibling; sibling = sibling.nextSibling) {
|
|
122
194
|
if (bucket && !getBucketsForNode(sibling).includes(bucket)) {
|
|
123
195
|
// eslint-disable-next-line no-continue
|
|
@@ -135,18 +207,10 @@ export class DependencyNotifyingDomFacade {
|
|
|
135
207
|
*
|
|
136
208
|
* @param node - the starting node
|
|
137
209
|
*/
|
|
138
|
-
// eslint-disable-next-line class-methods-use-this
|
|
139
210
|
getParentNode(node) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
parent &&
|
|
144
|
-
(parent.nodeType === Node.ELEMENT_NODE || parent.nodeType === Node.ATTRIBUTE_NODE)
|
|
145
|
-
) {
|
|
146
|
-
this._onNodeTouched(parent);
|
|
147
|
-
}
|
|
148
|
-
*/
|
|
149
|
-
return parent;
|
|
211
|
+
// Deliberately not notifying: navigating up must not register a dependency on the parent.
|
|
212
|
+
if (this._inner) return this._inner.getParentNode(node);
|
|
213
|
+
return node.parentNode;
|
|
150
214
|
}
|
|
151
215
|
|
|
152
216
|
/**
|
|
@@ -156,9 +220,14 @@ export class DependencyNotifyingDomFacade {
|
|
|
156
220
|
* @param node -
|
|
157
221
|
* @param bucket - The bucket that matches the attribute that will be used.
|
|
158
222
|
*/
|
|
159
|
-
// eslint-disable-next-line class-methods-use-this
|
|
160
223
|
getPreviousSibling(node, bucket) {
|
|
224
|
+
if (this._inner) {
|
|
225
|
+
const sibling = this._inner.getPreviousSibling(node, bucket);
|
|
226
|
+
if (sibling) this._onNodeTouched(sibling);
|
|
227
|
+
return sibling;
|
|
228
|
+
}
|
|
161
229
|
for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
|
|
230
|
+
// eslint-disable-next-line no-continue
|
|
162
231
|
if (bucket && !getBucketsForNode(sibling).includes(bucket)) continue;
|
|
163
232
|
this._onNodeTouched(sibling);
|
|
164
233
|
return sibling;
|
|
@@ -58,10 +58,23 @@ export default class DependentXPathQueries {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Detects whether any tracked XPath references one of the given variables.
|
|
62
62
|
*
|
|
63
|
-
*
|
|
63
|
+
* Pessimistic substring scan (`$name` also matches `$names`) — false positives are
|
|
64
|
+
* cheap extra refreshes, mirroring isInvalidatedByChildlistChanges.
|
|
65
|
+
*
|
|
66
|
+
* @param {string[]} varNames variable names without the leading '$'
|
|
64
67
|
*/
|
|
68
|
+
isInvalidatedByVariableChange(varNames) {
|
|
69
|
+
for (const xpath of this._xpaths) {
|
|
70
|
+
if (varNames.some(name => xpath.includes(`$${name}`))) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// We can also depend on these variables if they were used in our ancestry
|
|
75
|
+
return !!this._parentDependencies?.isInvalidatedByVariableChange(varNames);
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Add an XPath to the dependencies
|
|
67
80
|
*
|
package/src/ForeElementMixin.js
CHANGED
|
@@ -9,6 +9,13 @@ import getInScopeContext from './getInScopeContext.js';
|
|
|
9
9
|
import { Fore } from './fore.js';
|
|
10
10
|
import DependentXPathQueries from './DependentXPathQueries.js';
|
|
11
11
|
import { getPath } from './xpath-path.js';
|
|
12
|
+
import { DependencyNotifyingDomFacade } from './DependencyNotifyingDomFacade.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Matches any XPath function call, optionally prefixed (e.g. `count(`, `fn:string-join(`).
|
|
16
|
+
* Used by _refNeedsDependencyTracking after the navigation-anchor functions are stripped.
|
|
17
|
+
*/
|
|
18
|
+
const FUNCTION_CALL_RE = /[A-Za-z][\w.-]*(?::[A-Za-z][\w.-]*)?\s*\(/;
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Mixin containing all general functions that are shared by all Fore element classes.
|
|
@@ -66,6 +73,15 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
66
73
|
|
|
67
74
|
this.dependencies = new DependentXPathQueries();
|
|
68
75
|
this.ownerForm = null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* ModelItems this element observes because its ref expression read their nodes.
|
|
79
|
+
* Bucket-keyed: fx-control tracks two independent expressions (its own ref and its
|
|
80
|
+
* widget's ref) — a single Set would make each eval wrongly unregister the other's
|
|
81
|
+
* observers when diffing.
|
|
82
|
+
* @type {Map<string, Set<import('./modelitem.js').ModelItem>>}
|
|
83
|
+
*/
|
|
84
|
+
this._refTrackedModelItems = new Map();
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
getDebugInfo() {
|
|
@@ -134,6 +150,76 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
134
150
|
return null;
|
|
135
151
|
}
|
|
136
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Decides whether a ref expression needs generic dependency tracking. Navigation-only
|
|
155
|
+
* refs bind to exactly the node they return — attachObserver() already covers those.
|
|
156
|
+
* A ref that filters (predicate) or computes (any function call) reads nodes it never
|
|
157
|
+
* binds to, so its result can go stale without tracking. instance() and index() are
|
|
158
|
+
* navigation anchors, not computations: instance() only selects the evaluation root,
|
|
159
|
+
* and index() is handled by a dedicated 'item-changed' listener in UIElement.
|
|
160
|
+
* Restricted to elements with an `update()` observer callback (skips fx-bind).
|
|
161
|
+
* `$var`-only refs are covered by the variable-change consumer instead.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} [refString] the expression to check, defaults to this element's ref
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
_refNeedsDependencyTracking(refString = this.ref) {
|
|
167
|
+
if (!refString) return false;
|
|
168
|
+
if (typeof this.update !== 'function') return false;
|
|
169
|
+
if (refString.includes('[')) return true;
|
|
170
|
+
const stripped = refString.replace(/\b(?:instance|index)\s*\(/g, '');
|
|
171
|
+
return FUNCTION_CALL_RE.test(stripped);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolves a node touched during ref evaluation to its (possibly lazily created)
|
|
176
|
+
* ModelItem so this element can observe it.
|
|
177
|
+
*
|
|
178
|
+
* @param {import('./fx-model.js').FxModel} model
|
|
179
|
+
* @param {Node} touchedNode a node reported by DependencyNotifyingDomFacade
|
|
180
|
+
* @returns {import('./modelitem.js').ModelItem|null}
|
|
181
|
+
*/
|
|
182
|
+
_resolveModelItemForTouchedNode(model, touchedNode) {
|
|
183
|
+
let targetNode = touchedNode;
|
|
184
|
+
if (targetNode?.nodeType === Node.TEXT_NODE) targetNode = targetNode.parentNode;
|
|
185
|
+
if (!targetNode) return null;
|
|
186
|
+
return (
|
|
187
|
+
model.getModelItem(targetNode) ??
|
|
188
|
+
// the touched node may live in a different instance than the ref's primary one —
|
|
189
|
+
// resolve its instance per node, not from the expression
|
|
190
|
+
FxModel.lazyCreateModelItem(
|
|
191
|
+
model,
|
|
192
|
+
this.ref,
|
|
193
|
+
targetNode,
|
|
194
|
+
this,
|
|
195
|
+
model.getInstanceIdForNode(targetNode),
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Registers this element as observer on the ModelItems of all nodes its ref
|
|
202
|
+
* evaluation touched, and unregisters from ModelItems no longer read (per bucket).
|
|
203
|
+
*
|
|
204
|
+
* @param {Set<Node>} touchedNodes nodes reported by DependencyNotifyingDomFacade
|
|
205
|
+
* @param {string} [bucket] tracking bucket, e.g. 'ref' or 'widget'
|
|
206
|
+
*/
|
|
207
|
+
_trackRefDependencies(touchedNodes, bucket = 'ref') {
|
|
208
|
+
const model = this.getModel();
|
|
209
|
+
if (!model) return;
|
|
210
|
+
const newModelItems = new Set();
|
|
211
|
+
touchedNodes.forEach(n => {
|
|
212
|
+
const mi = this._resolveModelItemForTouchedNode(model, n);
|
|
213
|
+
if (mi) newModelItems.add(mi);
|
|
214
|
+
});
|
|
215
|
+
const previous = this._refTrackedModelItems.get(bucket);
|
|
216
|
+
previous?.forEach(mi => {
|
|
217
|
+
if (!newModelItems.has(mi)) mi.removeObserver(this);
|
|
218
|
+
});
|
|
219
|
+
newModelItems.forEach(mi => mi.addObserver(this));
|
|
220
|
+
this._refTrackedModelItems.set(bucket, newModelItems);
|
|
221
|
+
}
|
|
222
|
+
|
|
137
223
|
/**
|
|
138
224
|
* evaluation of fx-bind and UiElements differ in details so that each class needs it's own implementation.
|
|
139
225
|
*/
|
|
@@ -159,6 +245,13 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
159
245
|
// console.warn('using default context ', this);
|
|
160
246
|
// return;
|
|
161
247
|
}
|
|
248
|
+
let touchedNodes = null;
|
|
249
|
+
let domFacade = null;
|
|
250
|
+
if (this._refNeedsDependencyTracking()) {
|
|
251
|
+
touchedNodes = new Set();
|
|
252
|
+
domFacade = new DependencyNotifyingDomFacade(node => touchedNodes.add(node));
|
|
253
|
+
}
|
|
254
|
+
|
|
162
255
|
if (this.ref === '') {
|
|
163
256
|
this.nodeset = inscopeContext;
|
|
164
257
|
} else if (Array.isArray(inscopeContext)) {
|
|
@@ -174,17 +267,20 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
174
267
|
});
|
|
175
268
|
*/
|
|
176
269
|
// this.nodeset = evaluateXPathToFirstNode(this.ref, inscopeContext[0], this);
|
|
177
|
-
this.nodeset = evaluateXPath(this.ref, inscopeContext[0], this);
|
|
270
|
+
this.nodeset = evaluateXPath(this.ref, inscopeContext[0], this, {}, {}, domFacade);
|
|
178
271
|
} else {
|
|
179
272
|
// this.nodeset = fx.evaluateXPathToFirstNode(this.ref, inscopeContext, null, {namespaceResolver: this.namespaceResolver});
|
|
180
273
|
if (!inscopeContext) return;
|
|
181
274
|
if (this.nodeName === 'FX-REPEAT') {
|
|
182
275
|
// Repeats are special: they have multiple nodes in their nodeset
|
|
183
|
-
this.nodeset = evaluateXPath(this.ref, inscopeContext, this);
|
|
276
|
+
this.nodeset = evaluateXPath(this.ref, inscopeContext, this, {}, {}, domFacade);
|
|
184
277
|
} else {
|
|
185
|
-
this.nodeset = evaluateXPath(this.ref, inscopeContext, this)[0] || null;
|
|
278
|
+
this.nodeset = evaluateXPath(this.ref, inscopeContext, this, {}, {}, domFacade)[0] || null;
|
|
186
279
|
}
|
|
187
280
|
}
|
|
281
|
+
if (touchedNodes) {
|
|
282
|
+
this._trackRefDependencies(touchedNodes);
|
|
283
|
+
}
|
|
188
284
|
// console.log('UiElement evaluated to nodeset: ', this.nodeset);
|
|
189
285
|
}
|
|
190
286
|
|
|
@@ -314,22 +410,19 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
314
410
|
}
|
|
315
411
|
|
|
316
412
|
if (path) {
|
|
317
|
-
existed = model.
|
|
413
|
+
existed = model._modelItemsByPath.get(path) || null;
|
|
318
414
|
if (existed) {
|
|
319
415
|
// CRITICAL: retarget existing ModelItem to the current backing object
|
|
320
416
|
const isLensObject =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
existed
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
existed.node = targetNode;
|
|
331
|
-
existed.lens = null;
|
|
332
|
-
}
|
|
417
|
+
targetNode &&
|
|
418
|
+
typeof targetNode === 'object' &&
|
|
419
|
+
typeof targetNode.get === 'function' &&
|
|
420
|
+
typeof targetNode.set === 'function';
|
|
421
|
+
|
|
422
|
+
model._setModelItemTarget(
|
|
423
|
+
existed,
|
|
424
|
+
isLensObject ? { lens: targetNode } : { node: targetNode },
|
|
425
|
+
);
|
|
333
426
|
|
|
334
427
|
this.modelItem = existed;
|
|
335
428
|
return existed;
|
|
@@ -341,6 +434,7 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
341
434
|
this.modelItem = lazyCreatedModelItem;
|
|
342
435
|
return lazyCreatedModelItem;
|
|
343
436
|
}
|
|
437
|
+
|
|
344
438
|
/**
|
|
345
439
|
* Returns the effective value for the element.
|
|
346
440
|
* a: look for 'value' attribute and if present evaluate it and return the resulting value
|