@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Fore } from '../fore.js';
|
|
2
|
+
import { AbstractAction } from './abstract-action.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `fx-redo`
|
|
6
|
+
* re-applies the most recently undone action chain. Fires `redo-done` with
|
|
7
|
+
* `{canUndo, canRedo}` when a step was redone.
|
|
8
|
+
*
|
|
9
|
+
* @customElement
|
|
10
|
+
* @demo demo/undo-redo.html
|
|
11
|
+
*/
|
|
12
|
+
export class FxRedo extends AbstractAction {
|
|
13
|
+
async perform() {
|
|
14
|
+
// this element is a singleton reused across clicks - guard against a rapid
|
|
15
|
+
// double-click re-entering perform() while the first click's redo is still applying
|
|
16
|
+
if (this._busy) {
|
|
17
|
+
this.needsUpdate = false;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this._busy = true;
|
|
21
|
+
|
|
22
|
+
this.dispatchEvent(
|
|
23
|
+
new CustomEvent('execute-action', {
|
|
24
|
+
composed: true,
|
|
25
|
+
bubbles: true,
|
|
26
|
+
cancelable: true,
|
|
27
|
+
detail: { action: this, event: this.event },
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// commit any in-progress widget edit first (matters for non-click triggers
|
|
32
|
+
// that don't move focus, e.g. custom keyboard bindings)
|
|
33
|
+
this.getOwnerForm().flushPendingWidgetEdit?.();
|
|
34
|
+
|
|
35
|
+
const undoManager = this.getModel().getEffectiveUndoManager();
|
|
36
|
+
// restoring a snapshot must not record itself as a new undo step
|
|
37
|
+
undoManager.suspended = true;
|
|
38
|
+
this.needsUpdate = undoManager.redo();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
actionPerformed() {
|
|
42
|
+
if (this.needsUpdate) {
|
|
43
|
+
const model = this.getModel();
|
|
44
|
+
model.updateModel();
|
|
45
|
+
this.getOwnerForm().refresh(true);
|
|
46
|
+
Fore.dispatch(this, 'redo-done', {
|
|
47
|
+
canUndo: model.canUndo(),
|
|
48
|
+
canRedo: model.canRedo(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
this._busy = false;
|
|
52
|
+
this.dispatchActionPerformed();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!customElements.get('fx-redo')) {
|
|
57
|
+
window.customElements.define('fx-redo', FxRedo);
|
|
58
|
+
}
|
|
@@ -33,7 +33,7 @@ class FxRefresh extends AbstractAction {
|
|
|
33
33
|
this.getOwnerForm().refresh(true);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
if(this.hasAttribute('selector')){
|
|
36
|
+
if (this.hasAttribute('selector')) {
|
|
37
37
|
const target = document.querySelector(this.getAttribute('selector'));
|
|
38
38
|
if (target && Fore.isUiElement(target.nodeName) && typeof target.refresh === 'function') {
|
|
39
39
|
target.refresh(true);
|
|
@@ -44,7 +44,7 @@ class FxRefresh extends AbstractAction {
|
|
|
44
44
|
const targetId = this.getAttribute('control');
|
|
45
45
|
console.log(`### <<<<< refresh() control '${targetId}' >>>>>`);
|
|
46
46
|
let ctrl = resolveId(targetId, this);
|
|
47
|
-
if(!ctrl){
|
|
47
|
+
if (!ctrl) {
|
|
48
48
|
ctrl = document.querySelector(`#${targetId}`);
|
|
49
49
|
}
|
|
50
50
|
if (ctrl && Fore.isUiElement(ctrl.nodeName) && typeof ctrl.refresh === 'function') {
|
package/src/actions/fx-reload.js
CHANGED
|
@@ -46,7 +46,7 @@ export default class FxReplace extends AbstractAction {
|
|
|
46
46
|
|
|
47
47
|
actionPerformed() {
|
|
48
48
|
this.getModel().updateModel();
|
|
49
|
-
this.getOwnerForm().refresh(true); //todo: optimize and update only the affected subtree
|
|
49
|
+
this.getOwnerForm().refresh(true); // todo: optimize and update only the affected subtree
|
|
50
50
|
this.dispatchActionPerformed();
|
|
51
51
|
}
|
|
52
52
|
|
package/src/actions/fx-send.js
CHANGED
|
@@ -86,17 +86,39 @@ class FxSend extends AbstractAction {
|
|
|
86
86
|
submission.parameters.set('target', resolved);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Capture before submit.submit() runs: fx-submission#_handleResponse() itself already
|
|
90
|
+
// runs the full updateModel()+refresh(true) cycle for a replace="instance" response,
|
|
91
|
+
// unless the model wasn't inited yet (see the `model.inited` guard there) - in that one
|
|
92
|
+
// case it skips its own cycle and we still need to do it below. Reading `inited` now (not
|
|
93
|
+
// after submit()) avoids mistaking "became inited during this very submit()" for "was
|
|
94
|
+
// already inited", which would wrongly skip the only cycle that ran.
|
|
95
|
+
const modelWasInited = this.getModel().inited;
|
|
89
96
|
await submission.submit();
|
|
90
97
|
if (submission.replace === 'instance') {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
if (!modelWasInited) {
|
|
99
|
+
this.getModel().updateModel();
|
|
100
|
+
// todo: this bypasses observers...
|
|
101
|
+
this.getOwnerForm().refresh(true); // whole instance changes - full refresh necessary
|
|
102
|
+
// this.getOwnerForm().addToBatchedNotifications(this.getOwnerForm());
|
|
103
|
+
}
|
|
104
|
+
// needsUpdate is set (below, via actionPerformed override) so the undo hook records
|
|
105
|
+
// this replace as an undo step - it is NOT read by the default actionPerformed cycle
|
|
106
|
+
// here, since that would run a second, redundant recalculate/revalidate/refresh(false)
|
|
107
|
+
// on top of the full refresh(true) already done (either just above, or inside
|
|
108
|
+
// fx-submission#_handleResponse)
|
|
109
|
+
this.needsUpdate = true;
|
|
96
110
|
}
|
|
97
111
|
// if not of type fx-submission signal error
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
actionPerformed() {
|
|
115
|
+
// the instance-replace branch above already ran the full update+refresh cycle itself;
|
|
116
|
+
// skip the default gated cycle (see the comment at the needsUpdate assignment) while
|
|
117
|
+
// still calling dispatchActionPerformed() - the generic undo commit/discard hook in
|
|
118
|
+
// _finalizePerform() runs independently of this override and needs nothing extra here
|
|
119
|
+
this.dispatchActionPerformed();
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
_emitToChannel() {
|
|
101
123
|
const channel = this.getModel().querySelector(`#${this.connection}`);
|
|
102
124
|
if (channel === null) {
|
|
@@ -47,7 +47,7 @@ export default class FxSetattribute extends AbstractAction {
|
|
|
47
47
|
this.attrName = this.hasAttribute('name') ? this.getAttribute('name') : null;
|
|
48
48
|
this.attrValue = this.hasAttribute('value') ? this.getAttribute('value') : '';
|
|
49
49
|
if (!this.attrName) {
|
|
50
|
-
Fore.dispatch(
|
|
50
|
+
Fore.dispatch(this, 'error', { message: 'name or value not specified' });
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ export default class FxSetattribute extends AbstractAction {
|
|
|
55
55
|
super.perform();
|
|
56
56
|
const mi = this.getModelItem();
|
|
57
57
|
if (mi.node.nodeType !== Node.ELEMENT_NODE) {
|
|
58
|
-
Fore.dispatch(
|
|
58
|
+
await Fore.dispatch(this, 'error', { message: 'referenced item is not an element' });
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
mi.node.setAttribute(this.attrName, this.attrValue);
|
|
@@ -65,12 +65,16 @@ export default class FxSetattribute extends AbstractAction {
|
|
|
65
65
|
this,
|
|
66
66
|
null,
|
|
67
67
|
);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// IMPORTANT: registerModelItem may return an existing canonical ModelItem for the same path.
|
|
69
|
+
// Always use the returned instance for notifications.
|
|
70
|
+
const canonical = this.getOwnerForm().getModel().registerModelItem(newModelItem);
|
|
71
|
+
this.getOwnerForm().addToBatchedNotifications(canonical);
|
|
72
72
|
this.needsUpdate = true;
|
|
73
|
-
|
|
73
|
+
canonical.notify();
|
|
74
|
+
// On first-time attribute creation the ModelItem has no observers yet (no ref
|
|
75
|
+
// could have read a nonexistent attribute), so notify() alone reaches nobody.
|
|
76
|
+
// Signal a structural change so refs matching the owner element re-evaluate.
|
|
77
|
+
this.getOwnerForm().signalChangeToElement(mi.node.localName);
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Fore } from '../fore.js';
|
|
2
|
+
import { AbstractAction } from './abstract-action.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `fx-undo`
|
|
6
|
+
* restores the instance data of the model to the state before the last undoable
|
|
7
|
+
* action chain. Fires `undo-done` with `{canUndo, canRedo}` when a step was undone.
|
|
8
|
+
*
|
|
9
|
+
* @customElement
|
|
10
|
+
* @demo demo/undo-redo.html
|
|
11
|
+
*/
|
|
12
|
+
export class FxUndo extends AbstractAction {
|
|
13
|
+
async perform() {
|
|
14
|
+
// this element is a singleton reused across clicks - guard against a rapid
|
|
15
|
+
// double-click re-entering perform() while the first click's undo is still applying
|
|
16
|
+
if (this._busy) {
|
|
17
|
+
this.needsUpdate = false;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this._busy = true;
|
|
21
|
+
|
|
22
|
+
this.dispatchEvent(
|
|
23
|
+
new CustomEvent('execute-action', {
|
|
24
|
+
composed: true,
|
|
25
|
+
bubbles: true,
|
|
26
|
+
cancelable: true,
|
|
27
|
+
detail: { action: this, event: this.event },
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// commit any in-progress widget edit first (matters for non-click triggers
|
|
32
|
+
// that don't move focus, e.g. custom keyboard bindings)
|
|
33
|
+
this.getOwnerForm().flushPendingWidgetEdit?.();
|
|
34
|
+
|
|
35
|
+
const undoManager = this.getModel().getEffectiveUndoManager();
|
|
36
|
+
// restoring a snapshot must not record itself as a new undo step
|
|
37
|
+
undoManager.suspended = true;
|
|
38
|
+
this.needsUpdate = undoManager.undo();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
actionPerformed() {
|
|
42
|
+
if (this.needsUpdate) {
|
|
43
|
+
const model = this.getModel();
|
|
44
|
+
model.updateModel();
|
|
45
|
+
this.getOwnerForm().refresh(true);
|
|
46
|
+
Fore.dispatch(this, 'undo-done', {
|
|
47
|
+
canUndo: model.canUndo(),
|
|
48
|
+
canRedo: model.canRedo(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
this._busy = false;
|
|
52
|
+
this.dispatchActionPerformed();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!customElements.get('fx-undo')) {
|
|
57
|
+
window.customElements.define('fx-undo', FxUndo);
|
|
58
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createNamespaceResolver } from './xpath-evaluation.js';
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {{nameTest: string, predicates: string[]}} RawStep
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Determine whether a string is a valid Name
|
|
8
|
+
*
|
|
9
|
+
* @param {string} name
|
|
10
|
+
* @returns {boolean} whether the name is a valid one
|
|
11
|
+
*/
|
|
12
|
+
function isValidName(name) {
|
|
13
|
+
const result = new window.DOMParser().parseFromString(`<${name}/>`, 'application/xml');
|
|
14
|
+
return result.querySelector('parsererror') === null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} xpath - the xpath to tokenize to steps
|
|
19
|
+
*/
|
|
20
|
+
function splitSteps(xpath) {
|
|
21
|
+
/**
|
|
22
|
+
* @type {RawStep[]}
|
|
23
|
+
*/
|
|
24
|
+
const steps = [];
|
|
25
|
+
let nameTestScratch = '';
|
|
26
|
+
let predicates = [];
|
|
27
|
+
let predicateScratch = '';
|
|
28
|
+
let predicateDepth = 0;
|
|
29
|
+
|
|
30
|
+
for (const char of xpath.split('')) {
|
|
31
|
+
if (char === '[') {
|
|
32
|
+
predicateDepth += 1;
|
|
33
|
+
if (predicateDepth > 1) {
|
|
34
|
+
// Keep the `[` in nested predicates
|
|
35
|
+
predicateScratch += char;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char === ']') {
|
|
40
|
+
predicateDepth -= 1;
|
|
41
|
+
if (predicateDepth < 1) {
|
|
42
|
+
// Outer predicate closed. Write away scratch. Do not keep the ']'
|
|
43
|
+
predicates.push(predicateScratch.trim());
|
|
44
|
+
predicateScratch = '';
|
|
45
|
+
} else {
|
|
46
|
+
predicateScratch += char;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (predicateDepth > 0) {
|
|
51
|
+
// We are in a predicate. Just write away!
|
|
52
|
+
predicateScratch += char;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Predicate is zero at this point. Check if we are in a nametest or if we just stopped one
|
|
57
|
+
if (char === '/') {
|
|
58
|
+
// Consume this path step
|
|
59
|
+
if (nameTestScratch) {
|
|
60
|
+
steps.push({
|
|
61
|
+
nameTest: nameTestScratch.trim(),
|
|
62
|
+
predicates,
|
|
63
|
+
});
|
|
64
|
+
nameTestScratch = '';
|
|
65
|
+
predicates = [];
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
nameTestScratch += char;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (nameTestScratch) {
|
|
74
|
+
// Flush it
|
|
75
|
+
steps.push({
|
|
76
|
+
nameTest: nameTestScratch.trim(),
|
|
77
|
+
predicates,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return steps;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} possibleAttributeTest
|
|
86
|
+
*/
|
|
87
|
+
function extractAttributeTest(possibleAttributeTest) {
|
|
88
|
+
possibleAttributeTest = possibleAttributeTest.trim();
|
|
89
|
+
if (!possibleAttributeTest.startsWith('@')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const predicateRegex = /@(.*)\s*=\s*(['"])(.*)\2/g;
|
|
93
|
+
const match = predicateRegex.exec(possibleAttributeTest);
|
|
94
|
+
if (match) {
|
|
95
|
+
// Yep, this has the form `@type="my-type"`
|
|
96
|
+
return { name: match[1].trim(), value: match[3] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// This is of the form `@attr`
|
|
100
|
+
return { name: possibleAttributeTest.substring(1), value: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @typedef {{form: 'attributes', found: {name: string, value: string}} | {form: 'nested-path', found: RawStep[]}} ParsedPredicate
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} predicate - The clean predicate string, everything between the (outer) `[` and `]`
|
|
109
|
+
*
|
|
110
|
+
* @returns {ParsedPredicate}
|
|
111
|
+
*/
|
|
112
|
+
function parsePredicate(predicate) {
|
|
113
|
+
const attributeTest = extractAttributeTest(predicate);
|
|
114
|
+
if (attributeTest) {
|
|
115
|
+
// Yep, this has the form `@type="my-type"`
|
|
116
|
+
return {
|
|
117
|
+
form: 'attributes',
|
|
118
|
+
found: attributeTest,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// No. Is this a path?
|
|
123
|
+
const nestedPath = splitSteps(predicate);
|
|
124
|
+
if (nestedPath) {
|
|
125
|
+
return {
|
|
126
|
+
form: 'nested-path',
|
|
127
|
+
found: nestedPath,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Don't know.
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a structure from an XPath
|
|
136
|
+
*
|
|
137
|
+
* @param {string} xpath - The XPath to create the structure for
|
|
138
|
+
* @param {Element} baseElement - The parent of the new element
|
|
139
|
+
* @param {import('./fx-fore').FxFore} foreElement - The Fore element in which this is created. Used for namespace resolving
|
|
140
|
+
*/
|
|
141
|
+
export default function createNodes(xpath, baseElement, foreElement) {
|
|
142
|
+
const baseNamespace = baseElement?.namespaceURI || null;
|
|
143
|
+
const namespaceResolver = createNamespaceResolver(xpath, foreElement);
|
|
144
|
+
|
|
145
|
+
const ownerDoc = baseElement.ownerDocument;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} token
|
|
149
|
+
*/
|
|
150
|
+
const parseName = token => {
|
|
151
|
+
const raw = token.trim();
|
|
152
|
+
|
|
153
|
+
if (raw.startsWith('@')) {
|
|
154
|
+
const { name, value } = extractAttributeTest(raw);
|
|
155
|
+
|
|
156
|
+
if (name.startsWith('*:')) {
|
|
157
|
+
return { isAttribute: true, namespaceURI: null, localName: name.substring(2) };
|
|
158
|
+
}
|
|
159
|
+
if (name.includes(':')) {
|
|
160
|
+
const [prefix, localName] = name.split(':');
|
|
161
|
+
return {
|
|
162
|
+
isAttribute: true,
|
|
163
|
+
namespaceURI: prefix === '*' ? null : namespaceResolver(prefix) || null,
|
|
164
|
+
localName,
|
|
165
|
+
value,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { isAttribute: true, namespaceURI: null, localName: name, value };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (raw.startsWith('*:')) {
|
|
172
|
+
return { isAttribute: false, namespaceURI: baseNamespace, localName: raw.substring(2) };
|
|
173
|
+
}
|
|
174
|
+
if (raw.includes(':')) {
|
|
175
|
+
const [prefix, localName] = raw.split(':');
|
|
176
|
+
return {
|
|
177
|
+
isAttribute: false,
|
|
178
|
+
namespaceURI: prefix === '*' ? baseNamespace : namespaceResolver(prefix) || baseNamespace,
|
|
179
|
+
localName,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { isAttribute: false, namespaceURI: baseNamespace, localName: raw };
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const steps = splitSteps(xpath).filter(step => step.nameTest !== '.');
|
|
186
|
+
|
|
187
|
+
if (!steps.length) return null;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Process a single step
|
|
191
|
+
*
|
|
192
|
+
* @param {RawStep} rawStep
|
|
193
|
+
* @param {Element} current
|
|
194
|
+
* @param {boolean} isRoot - True if this is the 'root' expression, false if we are in a predicate already
|
|
195
|
+
*
|
|
196
|
+
* @returns {({action: 'continue', element: Element} | {action: 'abort'} | {action: 'return', attr: Attr})}
|
|
197
|
+
*/
|
|
198
|
+
const processStep = (rawStep, current, isRoot) => {
|
|
199
|
+
const { nameTest, predicates } = rawStep;
|
|
200
|
+
if (!nameTest || nameTest === '.') {
|
|
201
|
+
return {
|
|
202
|
+
action: 'continue',
|
|
203
|
+
element: current,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const parsed = parseName(nameTest);
|
|
208
|
+
|
|
209
|
+
if (!isValidName(parsed.localName)) {
|
|
210
|
+
// This did not result in a valid name. Stop.
|
|
211
|
+
console.warn(
|
|
212
|
+
`Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`,
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
action: 'abort',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (parsed.isAttribute) {
|
|
220
|
+
if (!current) {
|
|
221
|
+
const attr = ownerDoc.createAttribute(parsed.localName);
|
|
222
|
+
attr.value = parsed.value;
|
|
223
|
+
return {
|
|
224
|
+
action: 'return',
|
|
225
|
+
attr,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (isRoot && parsed.value !== null) {
|
|
229
|
+
// Prevent a path shaped like `./@value="a"` to just set an attribute. This should only be done in predicates
|
|
230
|
+
return {
|
|
231
|
+
action: 'abort',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
current.setAttribute(parsed.localName, parsed.value ?? '');
|
|
235
|
+
return {
|
|
236
|
+
action: 'continue',
|
|
237
|
+
element: current,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @type {Element}
|
|
243
|
+
*/
|
|
244
|
+
const element = parsed.namespaceURI
|
|
245
|
+
? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName)
|
|
246
|
+
: ownerDoc.createElement(parsed.localName);
|
|
247
|
+
|
|
248
|
+
for (const predicate of predicates) {
|
|
249
|
+
const parsedPredicate = parsePredicate(predicate);
|
|
250
|
+
if (!parsedPredicate) {
|
|
251
|
+
// This did not result in a valid name. Stop.
|
|
252
|
+
console.warn(
|
|
253
|
+
`Creating node for the XPath ${xpath} failed because the part ${predicates} could not be processed.`,
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
action: 'abort',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (parsedPredicate.form === 'attributes') {
|
|
260
|
+
const attrName = parsedPredicate.found.name.includes(':')
|
|
261
|
+
? parsedPredicate.found.name.split(':')[1]
|
|
262
|
+
: parsedPredicate.found.name;
|
|
263
|
+
element.setAttribute(attrName, parsedPredicate.found.value);
|
|
264
|
+
} else {
|
|
265
|
+
const nestedPath = parsedPredicate.found;
|
|
266
|
+
let subtree = element;
|
|
267
|
+
for (const step of nestedPath) {
|
|
268
|
+
const result = processStep(step, subtree, false);
|
|
269
|
+
if (result.action === 'abort') {
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (result.action === 'continue') {
|
|
274
|
+
subtree = result.element;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (current) {
|
|
281
|
+
current.appendChild(element);
|
|
282
|
+
}
|
|
283
|
+
current = element;
|
|
284
|
+
return {
|
|
285
|
+
action: 'continue',
|
|
286
|
+
element,
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
let current = null;
|
|
291
|
+
|
|
292
|
+
let subtreeRoot = null;
|
|
293
|
+
|
|
294
|
+
for (const rawStep of steps) {
|
|
295
|
+
const result = processStep(rawStep, current, true);
|
|
296
|
+
switch (result.action) {
|
|
297
|
+
case 'abort':
|
|
298
|
+
return null;
|
|
299
|
+
case 'return':
|
|
300
|
+
return subtreeRoot;
|
|
301
|
+
case 'continue':
|
|
302
|
+
if (!current) {
|
|
303
|
+
// This is the absolute root now
|
|
304
|
+
subtreeRoot = result.element;
|
|
305
|
+
}
|
|
306
|
+
current = result.element;
|
|
307
|
+
break;
|
|
308
|
+
|
|
309
|
+
default:
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return subtreeRoot;
|
|
314
|
+
}
|
package/src/fore.js
CHANGED
|
@@ -23,6 +23,33 @@ export class Fore {
|
|
|
23
23
|
|
|
24
24
|
static TYPE_DEFAULT = 'xs:string';
|
|
25
25
|
|
|
26
|
+
static _styleSheetCache = new Map();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a `CSSStyleSheet` for the given (static) CSS text, memoized by the CSS text itself so
|
|
30
|
+
* that a repeated element (e.g. `fx-output`/`fx-control` inside an `fx-repeat` with hundreds or
|
|
31
|
+
* thousands of rows) shares one parsed sheet via `shadowRoot.adoptedStyleSheets` instead of every
|
|
32
|
+
* instance re-parsing an identical `<style>` block via `shadowRoot.innerHTML`. Returns `null` when
|
|
33
|
+
* constructable stylesheets aren't supported so callers can fall back to inline `<style>`.
|
|
34
|
+
*
|
|
35
|
+
* @param cssText {String} - static CSS, must not contain per-instance interpolation
|
|
36
|
+
* @returns {CSSStyleSheet|null}
|
|
37
|
+
*/
|
|
38
|
+
static getSharedStyleSheet(cssText) {
|
|
39
|
+
if (typeof CSSStyleSheet === 'undefined') return null;
|
|
40
|
+
let sheet = Fore._styleSheetCache.get(cssText);
|
|
41
|
+
if (!sheet) {
|
|
42
|
+
try {
|
|
43
|
+
sheet = new CSSStyleSheet();
|
|
44
|
+
sheet.replaceSync(cssText);
|
|
45
|
+
Fore._styleSheetCache.set(cssText, sheet);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return sheet;
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
/**
|
|
27
54
|
* Loads and return a piece of HTML
|
|
28
55
|
* @param url {String} - the Url to load from
|
|
@@ -160,6 +187,32 @@ export class Fore {
|
|
|
160
187
|
return input;
|
|
161
188
|
}
|
|
162
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Evaluates all `{expr}` occurrences within a string against a given context node and
|
|
192
|
+
* form element, substituting each with its evaluated string value while leaving any
|
|
193
|
+
* surrounding static text untouched (e.g. `'foo {name}'` -> `'foo bar'`).
|
|
194
|
+
*
|
|
195
|
+
* Unlike `getExpression()`, this does not require the whole string to be a single
|
|
196
|
+
* expression - it supports static text mixed with one or more `{expr}` placeholders.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} input the string possibly containing one or more `{expr}` placeholders
|
|
199
|
+
* @param {Node} contextNode the node the expression(s) are evaluated against
|
|
200
|
+
* @param {HTMLElement} formElement the Fore element providing variable/namespace scope
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
static evaluateTemplateString(input, contextNode, formElement) {
|
|
204
|
+
return String(input ?? '').replace(/{[^}]*}/g, match => {
|
|
205
|
+
if (match === '{}') return match;
|
|
206
|
+
const naked = match.substring(1, match.length - 1);
|
|
207
|
+
try {
|
|
208
|
+
return evaluateXPathToString(naked, contextNode, formElement);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.warn('ignoring unparseable expr', error);
|
|
211
|
+
return match;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
163
216
|
/**
|
|
164
217
|
* returns the next `fx-fore` element upwards in tree
|
|
165
218
|
*
|
|
@@ -440,24 +493,6 @@ export class Fore {
|
|
|
440
493
|
return 'done';
|
|
441
494
|
}
|
|
442
495
|
|
|
443
|
-
/*
|
|
444
|
-
static evaluateAttributeTemplateExpression(expr, node) {
|
|
445
|
-
const matches = expr.match(/{[^}]*}/g);
|
|
446
|
-
if (matches) {
|
|
447
|
-
matches.forEach(match => {
|
|
448
|
-
console.log('match ', match);
|
|
449
|
-
const naked = match.substring(1, match.length - 1);
|
|
450
|
-
const inscope = getInScopeContext(node, naked);
|
|
451
|
-
const result = evaluateXPathToString(naked, inscope, node.getOwnerForm());
|
|
452
|
-
const replaced = expr.replaceAll(match, result);
|
|
453
|
-
console.log('replacing ', expr, ' with ', replaced);
|
|
454
|
-
expr = replaced;
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
return expr;
|
|
458
|
-
}
|
|
459
|
-
*/
|
|
460
|
-
|
|
461
496
|
static fadeInElement(element) {
|
|
462
497
|
const duration = 600;
|
|
463
498
|
let fadeIn = () => {
|
|
@@ -134,7 +134,7 @@ export class FxFunctionlib extends ForeElementMixin {
|
|
|
134
134
|
|
|
135
135
|
_ensurePrefixDeclared(prefix) {
|
|
136
136
|
const ownerForm =
|
|
137
|
-
|
|
137
|
+
(typeof this.getOwnerForm === 'function' && this.getOwnerForm()) || this.closest('fx-fore');
|
|
138
138
|
|
|
139
139
|
if (!ownerForm) return;
|
|
140
140
|
|
|
@@ -149,8 +149,8 @@ export class FxFunctionlib extends ForeElementMixin {
|
|
|
149
149
|
|
|
150
150
|
// If prefix is given: register ONLY the prefixed signature (no unprefixed alias).
|
|
151
151
|
const sig = prefix
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
? applyPrefixToSignature(functionObject.signature, prefix)
|
|
153
|
+
: functionObject.signature;
|
|
154
154
|
|
|
155
155
|
registerFunction({ ...functionObject, signature: sig }, this);
|
|
156
156
|
}
|
|
@@ -168,12 +168,12 @@ export class FxFunctionlib extends ForeElementMixin {
|
|
|
168
168
|
if (typeof signature !== 'string' || !signature.trim()) continue;
|
|
169
169
|
|
|
170
170
|
this._register(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
{
|
|
172
|
+
type: 'text/javascript',
|
|
173
|
+
signature: signature.trim(),
|
|
174
|
+
implementation: item,
|
|
175
|
+
},
|
|
176
|
+
prefix,
|
|
177
177
|
);
|
|
178
178
|
} else if (item && typeof item === 'object' && typeof item.signature === 'string') {
|
|
179
179
|
this._register(item, prefix);
|
|
@@ -209,4 +209,4 @@ export class FxFunctionlib extends ForeElementMixin {
|
|
|
209
209
|
|
|
210
210
|
if (!customElements.get('fx-functionlib')) {
|
|
211
211
|
customElements.define('fx-functionlib', FxFunctionlib);
|
|
212
|
-
}
|
|
212
|
+
}
|