@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/src/ui/fx-repeatitem.js
CHANGED
|
@@ -54,7 +54,10 @@ export class FxRepeatitem extends withDraggability(UIElement, true) {
|
|
|
54
54
|
// Its children already get their context from the repeatitem via getInScopeContext().
|
|
55
55
|
this.ref = `${this.parentNode.ref}`;
|
|
56
56
|
|
|
57
|
-
this.
|
|
57
|
+
this.setAttribute('role', 'listitem');
|
|
58
|
+
// `tabindex` (lowercase) is not a reflected IDL property - assigning it was a silent no-op.
|
|
59
|
+
// Use the actual attribute so repeat items are keyboard-focusable.
|
|
60
|
+
this.setAttribute('tabindex', '0');
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
disconnectedCallback() {
|
package/src/ui/fx-switch.js
CHANGED
|
@@ -21,20 +21,29 @@ class FxSwitch extends FxContainer {
|
|
|
21
21
|
this.selectedCase = null;
|
|
22
22
|
this.cases = null;
|
|
23
23
|
this.visitedResetted = false;
|
|
24
|
+
this.tabTriggers = [];
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
connectedCallback() {
|
|
27
28
|
if (super.connectedCallback) {
|
|
28
29
|
super.connectedCallback();
|
|
29
30
|
}
|
|
31
|
+
// Opt-in ARIA tabs pattern (role="tablist"/"tab"/"tabpanel", aria-selected, arrow-key
|
|
32
|
+
// navigation). Requires `fx-trigger` tab controls (each with an `fx-toggle` child targeting
|
|
33
|
+
// a sibling `fx-case`) to be direct children, so a valid tablist can be assembled - see
|
|
34
|
+
// ACCESSIBILITY.md. Plain fx-switch usage (bound switches, wizards, accordions built from
|
|
35
|
+
// interleaved trigger/case pairs) is unaffected.
|
|
36
|
+
this.tabsMode = this.getAttribute('appearance') === 'tabs';
|
|
37
|
+
|
|
30
38
|
const style = `
|
|
31
39
|
:host ::slotted(fx-case.selected-case){
|
|
32
40
|
display: block;
|
|
33
41
|
}
|
|
34
42
|
`;
|
|
35
|
-
const html =
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const html = this.tabsMode
|
|
44
|
+
? `<div part="tablist" role="tablist"><slot name="tab"></slot></div>
|
|
45
|
+
<slot></slot>`
|
|
46
|
+
: `<slot></slot>`;
|
|
38
47
|
this.shadowRoot.innerHTML = `
|
|
39
48
|
<style>
|
|
40
49
|
${style}
|
|
@@ -44,6 +53,11 @@ class FxSwitch extends FxContainer {
|
|
|
44
53
|
this.cases = [];
|
|
45
54
|
this.formerCase = null;
|
|
46
55
|
this.selectedCase = null;
|
|
56
|
+
this.tabTriggers = [];
|
|
57
|
+
|
|
58
|
+
if (this.tabsMode) {
|
|
59
|
+
this.addEventListener('keydown', e => this._handleTabKeydown(e));
|
|
60
|
+
}
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
async refresh(force) {
|
|
@@ -61,9 +75,104 @@ class FxSwitch extends FxContainer {
|
|
|
61
75
|
this.toggle(this.selectedCase);
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
if (this.tabsMode) {
|
|
79
|
+
this._setupTabs();
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
Fore.refreshChildren(this.selectedCase, force);
|
|
65
83
|
}
|
|
66
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Discovers direct-child `fx-trigger`/`fx-toggle` pairs that target this switch's cases and
|
|
87
|
+
* wires them up as an ARIA tab strip. Runs discovery once (mirroring the `this.cases` caching
|
|
88
|
+
* above); subsequent calls just re-sync the selected state.
|
|
89
|
+
*/
|
|
90
|
+
_setupTabs() {
|
|
91
|
+
if (this.tabTriggers.length > 0) {
|
|
92
|
+
this._syncTabSelection();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const triggers = Array.from(this.querySelectorAll(':scope > fx-trigger'));
|
|
97
|
+
this.tabTriggers = triggers
|
|
98
|
+
.map(trigger => {
|
|
99
|
+
const toggle = trigger.querySelector(':scope > fx-toggle[case]');
|
|
100
|
+
if (!toggle) return null;
|
|
101
|
+
const targetCase = this.cases.find(c => c.id === toggle.getAttribute('case'));
|
|
102
|
+
if (!targetCase) return null;
|
|
103
|
+
const widget = (trigger.getWidget && trigger.getWidget()) || trigger.firstElementChild;
|
|
104
|
+
if (!widget) return null;
|
|
105
|
+
return { trigger, targetCase, widget };
|
|
106
|
+
})
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
|
|
109
|
+
if (this.tabTriggers.length === 0) return;
|
|
110
|
+
|
|
111
|
+
this.tabTriggers.forEach(({ trigger, targetCase, widget }) => {
|
|
112
|
+
trigger.setAttribute('slot', 'tab');
|
|
113
|
+
targetCase.id = targetCase.id || `fx-case-${Fore.createUUID()}`;
|
|
114
|
+
widget.id = widget.id || `fx-tab-${Fore.createUUID()}`;
|
|
115
|
+
widget.setAttribute('role', 'tab');
|
|
116
|
+
widget.setAttribute('aria-controls', targetCase.id);
|
|
117
|
+
|
|
118
|
+
targetCase.setAttribute('role', 'tabpanel');
|
|
119
|
+
targetCase.setAttribute('aria-labelledby', widget.id);
|
|
120
|
+
if (!targetCase.hasAttribute('tabindex')) {
|
|
121
|
+
targetCase.setAttribute('tabindex', '0');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this._syncTabSelection();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Reflects the current `selectedCase` onto the tab strip: `aria-selected` plus a roving
|
|
130
|
+
* tabindex (selected tab is the only one reachable by Tab; arrow keys move between the rest).
|
|
131
|
+
*/
|
|
132
|
+
_syncTabSelection() {
|
|
133
|
+
this.tabTriggers.forEach(({ targetCase, widget }) => {
|
|
134
|
+
const selected = targetCase === this.selectedCase;
|
|
135
|
+
widget.setAttribute('aria-selected', String(selected));
|
|
136
|
+
widget.setAttribute('tabindex', selected ? '0' : '-1');
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Arrow-key navigation between tabs (Left/Right/Up/Down/Home/End), following the ARIA
|
|
142
|
+
* authoring practices "automatic activation" model - moving focus also selects the case.
|
|
143
|
+
*/
|
|
144
|
+
async _handleTabKeydown(e) {
|
|
145
|
+
const { tabTriggers } = this;
|
|
146
|
+
if (tabTriggers.length === 0) return;
|
|
147
|
+
|
|
148
|
+
const idx = tabTriggers.findIndex(({ widget }) => widget === e.target);
|
|
149
|
+
if (idx === -1) return;
|
|
150
|
+
|
|
151
|
+
let newIdx;
|
|
152
|
+
switch (e.key) {
|
|
153
|
+
case 'ArrowRight':
|
|
154
|
+
case 'ArrowDown':
|
|
155
|
+
newIdx = (idx + 1) % tabTriggers.length;
|
|
156
|
+
break;
|
|
157
|
+
case 'ArrowLeft':
|
|
158
|
+
case 'ArrowUp':
|
|
159
|
+
newIdx = (idx - 1 + tabTriggers.length) % tabTriggers.length;
|
|
160
|
+
break;
|
|
161
|
+
case 'Home':
|
|
162
|
+
newIdx = 0;
|
|
163
|
+
break;
|
|
164
|
+
case 'End':
|
|
165
|
+
newIdx = tabTriggers.length - 1;
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
const { trigger, widget } = tabTriggers[newIdx];
|
|
172
|
+
await trigger.performActions();
|
|
173
|
+
widget.focus();
|
|
174
|
+
}
|
|
175
|
+
|
|
67
176
|
_dispatchEvents() {
|
|
68
177
|
if (this.selectedCase === this.formerCase) return;
|
|
69
178
|
if (this.formerCase && this.formerCase !== this.selectedCase) {
|
|
@@ -150,6 +259,10 @@ class FxSwitch extends FxContainer {
|
|
|
150
259
|
this._dispatchEvents();
|
|
151
260
|
this.formerCase = this.selectedCase;
|
|
152
261
|
|
|
262
|
+
if (this.tabsMode) {
|
|
263
|
+
this._syncTabSelection();
|
|
264
|
+
}
|
|
265
|
+
|
|
153
266
|
// Tell the owner form we might have new template expressions here
|
|
154
267
|
this.getOwnerForm().scanForNewTemplateExpressionsNextRefresh();
|
|
155
268
|
this.getOwnerForm().getModel().updateModel();
|
package/src/ui/fx-trigger.js
CHANGED
|
@@ -26,7 +26,7 @@ export class FxTrigger extends XfAbstractControl {
|
|
|
26
26
|
if (!elements[0].getAttribute('tabindex')) {
|
|
27
27
|
elements[0].setAttribute('tabindex', '0');
|
|
28
28
|
}
|
|
29
|
-
if (elements[0].nodeName !== 'BUTTON') {
|
|
29
|
+
if (elements[0].nodeName !== 'BUTTON' && !elements[0].getAttribute('role')) {
|
|
30
30
|
elements[0].setAttribute('role', 'button');
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -98,11 +98,16 @@ export class FxTrigger extends XfAbstractControl {
|
|
|
98
98
|
repeatedItem.click();
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Update all child variables, but only once
|
|
102
|
-
this.querySelectorAll('fx-var').forEach(variableElement => variableElement.refresh());
|
|
103
|
-
|
|
104
101
|
for (let i = 0; i < this.children.length; i += 1) {
|
|
105
102
|
const child = this.children[i];
|
|
103
|
+
// XForms 2.0: action variables are evaluated in sequence between sibling
|
|
104
|
+
// actions — a variable after an action sees that action's effects. Variables
|
|
105
|
+
// nested inside an fx-action child are evaluated by fx-action.perform().
|
|
106
|
+
if (child.localName === 'fx-var') {
|
|
107
|
+
child.refresh();
|
|
108
|
+
// eslint-disable-next-line no-continue
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
106
111
|
if (typeof child.execute === 'function') {
|
|
107
112
|
if (e) {
|
|
108
113
|
// We are handling the event. Stop it from going further
|
package/src/ui/fx-upload.js
CHANGED
|
@@ -39,6 +39,7 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
|
|
42
43
|
connectedCallback() {
|
|
43
44
|
this.updateEvent = 'change';
|
|
44
45
|
this.accept = this.hasAttribute('accept') ? this.getAttribute('accept') : '';
|
|
@@ -68,7 +69,7 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
// console.log('widget ', this.widget);
|
|
71
|
-
|
|
72
|
+
const listenOn = this.widget; // default: usually listening on widget
|
|
72
73
|
|
|
73
74
|
// ### convenience marker event
|
|
74
75
|
if (this.debounceDelay) {
|
|
@@ -137,7 +138,7 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
137
138
|
const readMethod = isTextFile ? 'readAsText' : 'readAsDataURL';
|
|
138
139
|
|
|
139
140
|
reader.onload = () => {
|
|
140
|
-
const result = reader
|
|
141
|
+
const { result } = reader;
|
|
141
142
|
// If it's a binary file, return only the Base64 content without the MIME prefix
|
|
142
143
|
if (!isTextFile) {
|
|
143
144
|
const base64Content = result.split(',')[1]; // Remove MIME prefix
|
|
@@ -163,6 +164,10 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
163
164
|
* @param val the new value to be set
|
|
164
165
|
*/
|
|
165
166
|
setValue(val) {
|
|
167
|
+
const model = this.getModel();
|
|
168
|
+
const undoManager = model?.getEffectiveUndoManager();
|
|
169
|
+
undoManager?.beginCapture();
|
|
170
|
+
|
|
166
171
|
this.value = val;
|
|
167
172
|
const modelitem = this.getModelItem();
|
|
168
173
|
|
|
@@ -189,6 +194,7 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
189
194
|
|
|
190
195
|
if (modelitem?.readonly) {
|
|
191
196
|
console.warn('attempt to change readonly node', modelitem);
|
|
197
|
+
undoManager?.discard();
|
|
192
198
|
return; // do nothing when modelItem is readonly
|
|
193
199
|
}
|
|
194
200
|
|
|
@@ -200,7 +206,6 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
200
206
|
this.modelItem.boundControls.push(this);
|
|
201
207
|
}
|
|
202
208
|
*/
|
|
203
|
-
const model = this.getModel();
|
|
204
209
|
Fore.dispatch(this, 'value-changed', {
|
|
205
210
|
path: this.modelItem.path,
|
|
206
211
|
value: this.modelItem.value,
|
|
@@ -208,8 +213,9 @@ export default class FxUpload extends XfAbstractControl {
|
|
|
208
213
|
instanceId: this.modelItem.instanceId,
|
|
209
214
|
foreId: this.getOwnerForm().id,
|
|
210
215
|
});
|
|
211
|
-
|
|
216
|
+
model.updateModel();
|
|
212
217
|
this.getOwnerForm().refresh(true);
|
|
218
|
+
undoManager?.commit(modelitem?.node);
|
|
213
219
|
|
|
214
220
|
// console.log('data', this.getOwnerForm().getModel().getDefaultInstanceData());
|
|
215
221
|
}
|
package/src/ui/repeat-base.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Fore } from '../fore.js';
|
|
18
18
|
import { evaluateXPath } from '../xpath-evaluation.js';
|
|
19
|
+
import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade.js';
|
|
19
20
|
import getInScopeContext from '../getInScopeContext.js';
|
|
20
21
|
import { withDraggability } from '../withDraggability.js';
|
|
21
22
|
import { UIElement } from './UIElement.js';
|
|
@@ -179,6 +180,7 @@ export class RepeatBase extends withDraggability(UIElement, false) {
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
disconnectedCallback() {
|
|
183
|
+
if (super.disconnectedCallback) super.disconnectedCallback();
|
|
182
184
|
this.getOwnerForm().removeEventListener('deleted', this.handleDelete);
|
|
183
185
|
this.getOwnerForm().removeEventListener('insert', this.handleInsert);
|
|
184
186
|
}
|
|
@@ -217,6 +219,7 @@ export class RepeatBase extends withDraggability(UIElement, false) {
|
|
|
217
219
|
|
|
218
220
|
this.getOwnerForm().addToBatchedNotifications(newRepeatItem);
|
|
219
221
|
}
|
|
222
|
+
|
|
220
223
|
/**
|
|
221
224
|
* @abstract
|
|
222
225
|
*
|
|
@@ -253,17 +256,19 @@ export class RepeatBase extends withDraggability(UIElement, false) {
|
|
|
253
256
|
});
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
let touchedNodes = null;
|
|
260
|
+
let domFacade = null;
|
|
261
|
+
if (this._refNeedsDependencyTracking()) {
|
|
262
|
+
touchedNodes = new Set();
|
|
263
|
+
domFacade = new DependencyNotifyingDomFacade(node => touchedNodes.add(node));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const rawNodeset = evaluateXPath(this.ref, inscope, this, {}, {}, domFacade);
|
|
267
|
+
|
|
268
|
+
if (touchedNodes) {
|
|
269
|
+
this._trackRefDependencies(touchedNodes);
|
|
270
|
+
}
|
|
271
|
+
|
|
267
272
|
if (rawNodeset.length === 1 && Array.isArray(rawNodeset[0])) {
|
|
268
273
|
// This XPath likely returned an XPath array. Just collapse to that array
|
|
269
274
|
this.nodeset = rawNodeset[0];
|
|
@@ -447,7 +452,10 @@ export class RepeatBase extends withDraggability(UIElement, false) {
|
|
|
447
452
|
if (mi.path.startsWith(`${parentBaseInChildStyle}_`)) return;
|
|
448
453
|
|
|
449
454
|
// Inject _op immediately after the parent base segment
|
|
450
|
-
|
|
455
|
+
this.getModel()._setModelItemPath(
|
|
456
|
+
mi,
|
|
457
|
+
`${parentBaseInChildStyle}_${op}${mi.path.slice(parentBaseInChildStyle.length)}`,
|
|
458
|
+
);
|
|
451
459
|
};
|
|
452
460
|
|
|
453
461
|
if (parentNode.attachObserver) {
|
package/src/withDraggability.js
CHANGED
|
@@ -177,6 +177,10 @@ export const withDraggability = (superclass, isAlsoDraggable) =>
|
|
|
177
177
|
this.replaceChildren(draggedItem);
|
|
178
178
|
}
|
|
179
179
|
*/
|
|
180
|
+
// NOTE: this branch (fx-droptarget) reorders live DOM/UI elements directly, not
|
|
181
|
+
// instance data - there is nothing here for UndoManager's instance-data snapshots
|
|
182
|
+
// to capture, so it is intentionally not wrapped with undo capture (see the
|
|
183
|
+
// dataNode branch below for the case that IS undoable).
|
|
180
184
|
event.preventDefault();
|
|
181
185
|
this.getOwnerForm().getModel().updateModel();
|
|
182
186
|
this.getOwnerForm().refresh(true);
|
|
@@ -187,6 +191,10 @@ export const withDraggability = (superclass, isAlsoDraggable) =>
|
|
|
187
191
|
return;
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
const model = this.getOwnerForm().getModel();
|
|
195
|
+
const undoManager = model.getEffectiveUndoManager();
|
|
196
|
+
undoManager.beginCapture();
|
|
197
|
+
|
|
190
198
|
if (this.localName === 'fx-repeat') {
|
|
191
199
|
// We are sure we'll handle this event!
|
|
192
200
|
event.preventDefault();
|
|
@@ -217,7 +225,8 @@ export const withDraggability = (superclass, isAlsoDraggable) =>
|
|
|
217
225
|
}
|
|
218
226
|
|
|
219
227
|
// Note: full refresh needed since multiple model items may be affected.
|
|
220
|
-
|
|
228
|
+
model.updateModel();
|
|
221
229
|
this.getOwnerForm().refresh(true);
|
|
230
|
+
undoManager.commit(dataNode);
|
|
222
231
|
}
|
|
223
232
|
};
|
package/src/xpath-evaluation.js
CHANGED
|
@@ -1360,7 +1360,7 @@ export function evaluateXPath(
|
|
|
1360
1360
|
return fxEvaluateXPath(
|
|
1361
1361
|
expr0,
|
|
1362
1362
|
contextNode,
|
|
1363
|
-
__jsonDomFacade,
|
|
1363
|
+
domFacade ? domFacade.withInner(__jsonDomFacade) : __jsonDomFacade,
|
|
1364
1364
|
{ ...variablesInScope, ...variables },
|
|
1365
1365
|
fxEvaluateXPath.ALL_RESULTS_TYPE,
|
|
1366
1366
|
{
|
|
@@ -1781,14 +1781,20 @@ export function evaluateXPathToString(xpath, contextNode, formElement, domFacade
|
|
|
1781
1781
|
// JSON context, non-lookup => evaluate via JSON facade
|
|
1782
1782
|
if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
|
|
1783
1783
|
const variablesInScope = getVariablesInScope(formElement);
|
|
1784
|
-
const res = fxEvaluateXPathToString(
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1784
|
+
const res = fxEvaluateXPathToString(
|
|
1785
|
+
expr0,
|
|
1786
|
+
contextNode,
|
|
1787
|
+
domFacade ? domFacade.withInner(__jsonDomFacade) : __jsonDomFacade,
|
|
1788
|
+
variablesInScope,
|
|
1789
|
+
{
|
|
1790
|
+
currentContext: { formElement },
|
|
1791
|
+
functionNameResolver,
|
|
1792
|
+
moduleImports: { xf: XFORMS_NAMESPACE_URI },
|
|
1793
|
+
namespaceResolver: null,
|
|
1794
|
+
language: Language.XPATH_3_1_LANGUAGE,
|
|
1795
|
+
xmlSerializer: new XMLSerializer(),
|
|
1796
|
+
},
|
|
1797
|
+
);
|
|
1792
1798
|
return stringify(res);
|
|
1793
1799
|
}
|
|
1794
1800
|
|
|
@@ -1906,7 +1912,7 @@ export function evaluateXPathToStrings(xpath, contextNode, formElement, domFacad
|
|
|
1906
1912
|
const res = fxEvaluateXPathToStrings(
|
|
1907
1913
|
expr0,
|
|
1908
1914
|
contextNode,
|
|
1909
|
-
__jsonDomFacade,
|
|
1915
|
+
domFacade ? domFacade.withInner(__jsonDomFacade) : __jsonDomFacade,
|
|
1910
1916
|
getVariablesInScope(formElement),
|
|
1911
1917
|
{
|
|
1912
1918
|
currentContext: { formElement },
|
|
@@ -1986,14 +1992,20 @@ export function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade
|
|
|
1986
1992
|
|
|
1987
1993
|
if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
|
|
1988
1994
|
const variablesInScope = getVariablesInScope(formElement);
|
|
1989
|
-
return fxEvaluateXPathToNumber(
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1995
|
+
return fxEvaluateXPathToNumber(
|
|
1996
|
+
expr0,
|
|
1997
|
+
contextNode,
|
|
1998
|
+
domFacade ? domFacade.withInner(__jsonDomFacade) : __jsonDomFacade,
|
|
1999
|
+
variablesInScope,
|
|
2000
|
+
{
|
|
2001
|
+
currentContext: { formElement },
|
|
2002
|
+
functionNameResolver,
|
|
2003
|
+
moduleImports: { xf: XFORMS_NAMESPACE_URI },
|
|
2004
|
+
namespaceResolver: null,
|
|
2005
|
+
language: Language.XPATH_3_1_LANGUAGE,
|
|
2006
|
+
xmlSerializer: new XMLSerializer(),
|
|
2007
|
+
},
|
|
2008
|
+
);
|
|
1997
2009
|
}
|
|
1998
2010
|
|
|
1999
2011
|
if (_looksLikeLookupExpr(expr0)) {
|
package/src/xpath-path.js
CHANGED
|
@@ -14,11 +14,130 @@ function shortenPath(path) {
|
|
|
14
14
|
return tmp1.substring(tmp1.indexOf('/'), tmp.length);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// --- fast native path computation -------------------------------------------------
|
|
18
|
+
//
|
|
19
|
+
// fontoxpath's `path()` function is correct but calling it once per node has real fixed
|
|
20
|
+
// overhead (compiling/interpreting the query, building a dynamic context, walking the
|
|
21
|
+
// domFacade) that dominates in practice over the tree walk itself. getPath()/getDocPath()
|
|
22
|
+
// are called once per node during bind-graph construction (fx-bind.js's _buildBindGraph())
|
|
23
|
+
// AND once per node inside any `iterate` action (fx-setattribute.js, fx-setvalue.js) - so a
|
|
24
|
+
// large sibling list (eg. an 800-row genericode codelist) previously meant 800+ separate
|
|
25
|
+
// fontoxpath `path()` evaluations, each re-scanning preceding siblings to compute the index
|
|
26
|
+
// -- measured at several seconds for UNTDID 1001 (802 Row elements) in
|
|
27
|
+
// demo/codelists/codelist-editor.html.
|
|
28
|
+
//
|
|
29
|
+
// nativeDocPath() below replicates the exact string format `path()` produces (post
|
|
30
|
+
// shortenPath) using plain DOM traversal - same asymptotic sibling-counting cost as
|
|
31
|
+
// fontoxpath's own implementation, but without the XPath-engine overhead per call. It
|
|
32
|
+
// returns `null` for anything it isn't confident matches fontoxpath's behavior exactly (eg.
|
|
33
|
+
// exotic node types); callers fall back to the original fontoxpath-based path in that case,
|
|
34
|
+
// so correctness never depends on this function handling every possible node shape.
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Identity used to count preceding siblings the way fn:path() does: same element name
|
|
38
|
+
* *and* namespace, or same text()/comment()/processing-instruction(target) kind. Two
|
|
39
|
+
* same-localName elements in different namespaces are (correctly) not siblings of the
|
|
40
|
+
* same "kind" here, matching fontoxpath indexing same-namespace elements independently.
|
|
41
|
+
* @param {Node} node
|
|
42
|
+
* @returns {string|null}
|
|
43
|
+
*/
|
|
44
|
+
function segmentKey(node) {
|
|
45
|
+
switch (node.nodeType) {
|
|
46
|
+
case Node.ELEMENT_NODE:
|
|
47
|
+
return `E|${node.namespaceURI || ''}|${node.localName || node.nodeName}`;
|
|
48
|
+
case Node.TEXT_NODE:
|
|
49
|
+
case Node.CDATA_SECTION_NODE:
|
|
50
|
+
return 'T';
|
|
51
|
+
case Node.COMMENT_NODE:
|
|
52
|
+
return 'C';
|
|
53
|
+
case Node.PROCESSING_INSTRUCTION_NODE:
|
|
54
|
+
return `P|${node.target}`;
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {Node} node
|
|
62
|
+
* @returns {string|null}
|
|
63
|
+
*/
|
|
64
|
+
function segmentLabel(node) {
|
|
65
|
+
switch (node.nodeType) {
|
|
66
|
+
case Node.ELEMENT_NODE:
|
|
67
|
+
return node.localName || node.nodeName;
|
|
68
|
+
case Node.TEXT_NODE:
|
|
69
|
+
case Node.CDATA_SECTION_NODE:
|
|
70
|
+
return 'text()';
|
|
71
|
+
case Node.COMMENT_NODE:
|
|
72
|
+
return 'comment()';
|
|
73
|
+
case Node.PROCESSING_INSTRUCTION_NODE:
|
|
74
|
+
return `processing-instruction(${node.target})`;
|
|
75
|
+
default:
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 1-based index among preceding siblings sharing the same `segmentKey`.
|
|
82
|
+
* @param {Node} node
|
|
83
|
+
* @param {string} key
|
|
84
|
+
* @returns {number}
|
|
85
|
+
*/
|
|
86
|
+
function siblingIndex(node, key) {
|
|
87
|
+
let index = 1;
|
|
88
|
+
let sib = node.previousSibling;
|
|
89
|
+
while (sib) {
|
|
90
|
+
if (segmentKey(sib) === key) index += 1;
|
|
91
|
+
sib = sib.previousSibling;
|
|
92
|
+
}
|
|
93
|
+
return index;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {Node} node
|
|
98
|
+
* @returns {string|null} the fontoxpath-`path()`-equivalent doc path (eg. `/a[1]/b[2]`,
|
|
99
|
+
* always starting with `/`), or `null` if this node shape isn't one this function is
|
|
100
|
+
* confident about - the caller should fall back to the fontoxpath-based computation.
|
|
101
|
+
*/
|
|
102
|
+
function nativeDocPath(node) {
|
|
103
|
+
if (!node || node.nodeType === undefined) return null;
|
|
104
|
+
|
|
105
|
+
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
106
|
+
const owner = node.ownerElement;
|
|
107
|
+
if (!owner) return null;
|
|
108
|
+
const ownerPath = nativeDocPath(owner);
|
|
109
|
+
if (ownerPath === null) return null;
|
|
110
|
+
return `${ownerPath}/@${node.localName || node.name}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (node.nodeType === Node.DOCUMENT_NODE) return '/';
|
|
114
|
+
|
|
115
|
+
// Build the segment chain from `node` up to (and including) the outermost ancestor
|
|
116
|
+
// that isn't a document node, then drop that outermost segment - matching the "cut-off
|
|
117
|
+
// root node ref" behavior of the original fontoxpath-based shortenPath().
|
|
118
|
+
const chain = [];
|
|
119
|
+
let current = node;
|
|
120
|
+
while (current && current.nodeType !== Node.DOCUMENT_NODE) {
|
|
121
|
+
const key = segmentKey(current);
|
|
122
|
+
const label = segmentLabel(current);
|
|
123
|
+
if (key === null || label === null) return null;
|
|
124
|
+
chain.push(`${label}[${siblingIndex(current, key)}]`);
|
|
125
|
+
current = current.parentNode;
|
|
126
|
+
}
|
|
127
|
+
chain.reverse();
|
|
128
|
+
|
|
129
|
+
if (chain.length > 1) chain.shift();
|
|
130
|
+
return chain.length ? `/${chain.join('/')}` : '/';
|
|
131
|
+
}
|
|
132
|
+
|
|
17
133
|
/**
|
|
18
134
|
* @param {Node} node
|
|
19
135
|
* @returns string
|
|
20
136
|
*/
|
|
21
137
|
export function getDocPath(node) {
|
|
138
|
+
const native = nativeDocPath(node);
|
|
139
|
+
if (native !== null) return native;
|
|
140
|
+
|
|
22
141
|
const path = fx.evaluateXPathToString('path()', node);
|
|
23
142
|
// Path is like `$default/x[1]/y[1]`
|
|
24
143
|
const shortened = shortenPath(path);
|
|
@@ -101,6 +220,9 @@ export function getPath(node, instanceId = 'default') {
|
|
|
101
220
|
}
|
|
102
221
|
|
|
103
222
|
function getXmlPath(node, instanceId) {
|
|
223
|
+
const native = nativeDocPath(node);
|
|
224
|
+
if (native !== null) return `$${instanceId}${native}`;
|
|
225
|
+
|
|
104
226
|
const path = fx.evaluateXPathToString('path()', node);
|
|
105
227
|
// Path is like `$default/x[1]/y[1]`
|
|
106
228
|
const shortened = shortenPath(path);
|