@jinntec/fore 3.0.1 → 3.1.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/README.md +0 -2
- package/dist/fore-dev.js +5072 -4180
- package/dist/fore.js +4819 -4165
- package/index.js +1 -0
- package/package.json +2 -2
- package/src/actions/fx-insert.js +3 -2
- package/src/authoring-check.js +231 -0
- package/src/fore.js +9 -11
- package/src/fx-fore.js +546 -147
- package/src/fx-instance.js +7 -1
- package/src/fx-speech.js +268 -0
- package/src/ui/abstract-control.js +14 -7
- package/src/ui/fx-case.js +3 -1
- package/src/ui/fx-control.js +4 -2
- package/src/ui/fx-group.js +1 -1
- package/src/ui/fx-output.js +8 -38
- package/src/ui/fx-repeat-attributes.js +1 -1
- package/src/ui/fx-repeat.js +1 -1
- package/src/ui/repeat-base.js +4 -4
package/src/fx-fore.js
CHANGED
|
@@ -2,7 +2,11 @@ import { Fore } from './fore.js';
|
|
|
2
2
|
import './fx-instance.js';
|
|
3
3
|
import { FxModel } from './fx-model.js';
|
|
4
4
|
import '@jinntec/jinn-toast';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
evaluateXPathToNodes,
|
|
7
|
+
evaluateXPathToString,
|
|
8
|
+
createNamespaceResolver,
|
|
9
|
+
} from './xpath-evaluation.js';
|
|
6
10
|
import getInScopeContext from './getInScopeContext.js';
|
|
7
11
|
import { XPathUtil } from './xpath-util.js';
|
|
8
12
|
import { FxRepeatAttributes } from './ui/fx-repeat-attributes.js';
|
|
@@ -22,9 +26,18 @@ const dirtyStates = {
|
|
|
22
26
|
};
|
|
23
27
|
async function waitForFunctionLibs(rootEl) {
|
|
24
28
|
const libs = Array.from(rootEl.querySelectorAll('fx-functionlib'));
|
|
25
|
-
await Promise.all(
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
await Promise.all(libs.map(l => (l.readyPromise ? l.readyPromise : Promise.resolve())));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Determine whether a string is a valid Name
|
|
34
|
+
*
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @returns {boolean} whether the name is a valid one
|
|
37
|
+
*/
|
|
38
|
+
function isValidName(name) {
|
|
39
|
+
const result = new DOMParser().parseFromString(`<${name}/>`, 'application/xml');
|
|
40
|
+
return result.querySelector('parsererror') === null;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
/**
|
|
@@ -222,8 +235,6 @@ export class FxFore extends HTMLElement {
|
|
|
222
235
|
right:0;
|
|
223
236
|
left:0;
|
|
224
237
|
height:40px;
|
|
225
|
-
border-radius: 5px;
|
|
226
|
-
|
|
227
238
|
}
|
|
228
239
|
.popup .close {
|
|
229
240
|
position: absolute;
|
|
@@ -248,6 +259,53 @@ export class FxFore extends HTMLElement {
|
|
|
248
259
|
.warning{
|
|
249
260
|
background:orange;
|
|
250
261
|
}
|
|
262
|
+
#authoringErrors {
|
|
263
|
+
z-index: 20;
|
|
264
|
+
}
|
|
265
|
+
#authoringErrors .popup {
|
|
266
|
+
width: 70%;
|
|
267
|
+
max-height: 80vh;
|
|
268
|
+
overflow:hidden;
|
|
269
|
+
}
|
|
270
|
+
#authoringErrors h2 {
|
|
271
|
+
background: #c62828;
|
|
272
|
+
color: white;
|
|
273
|
+
padding-left: 12px;
|
|
274
|
+
line-height: 40px;
|
|
275
|
+
font-size: 1rem;
|
|
276
|
+
}
|
|
277
|
+
#authoringErrors table {
|
|
278
|
+
width: 100%;
|
|
279
|
+
border-collapse: collapse;
|
|
280
|
+
font-size: 0.85rem;
|
|
281
|
+
}
|
|
282
|
+
#authoringErrors th {
|
|
283
|
+
text-align: left;
|
|
284
|
+
border-bottom: 2px solid #c62828;
|
|
285
|
+
padding: 4px 8px;
|
|
286
|
+
}
|
|
287
|
+
#authoringErrors td {
|
|
288
|
+
padding: 4px 8px;
|
|
289
|
+
border-bottom: 1px solid #ddd;
|
|
290
|
+
vertical-align: top;
|
|
291
|
+
}
|
|
292
|
+
#authoringErrors td:first-child {
|
|
293
|
+
color: #555;
|
|
294
|
+
font-family: monospace;
|
|
295
|
+
white-space: nowrap;
|
|
296
|
+
}
|
|
297
|
+
#authoringErrors .ae-actions {
|
|
298
|
+
text-align: center;
|
|
299
|
+
margin-top: 12px;
|
|
300
|
+
}
|
|
301
|
+
#authoringErrors .ae-actions button {
|
|
302
|
+
padding: 6px 20px;
|
|
303
|
+
background: #c62828;
|
|
304
|
+
color: white;
|
|
305
|
+
border: none;
|
|
306
|
+
border-radius: 3px;
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
}
|
|
251
309
|
`;
|
|
252
310
|
|
|
253
311
|
const html = `
|
|
@@ -265,6 +323,14 @@ export class FxFore extends HTMLElement {
|
|
|
265
323
|
<div id="messageContent"></div>
|
|
266
324
|
</div>
|
|
267
325
|
</div>
|
|
326
|
+
<div id="authoringErrors" class="overlay">
|
|
327
|
+
<div class="popup">
|
|
328
|
+
<h2>Authoring Errors</h2>
|
|
329
|
+
<a class="close" href="#" onclick="event.preventDefault();event.target.closest('.overlay').classList.remove('show')">×</a>
|
|
330
|
+
<div id="authoringErrorsContent" style="margin-top:48px;"></div>
|
|
331
|
+
<div class="ae-actions"><button onclick="this.closest('.overlay').classList.remove('show')">Dismiss</button></div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
268
334
|
<slot name="event"></slot>
|
|
269
335
|
`;
|
|
270
336
|
|
|
@@ -281,8 +347,8 @@ export class FxFore extends HTMLElement {
|
|
|
281
347
|
this._scanForNewTemplateExpressionsNextRefresh = false;
|
|
282
348
|
this.repeatsFromAttributesCreated = false;
|
|
283
349
|
this.validateOn = this.hasAttribute('validate-on')
|
|
284
|
-
|
|
285
|
-
|
|
350
|
+
? this.getAttribute('validate-on')
|
|
351
|
+
: 'update';
|
|
286
352
|
// this.mergePartial = this.hasAttribute('merge-partial')? true:false;
|
|
287
353
|
this.mergePartial = false;
|
|
288
354
|
this.createNodes = this.hasAttribute('create-nodes') ? true : false;
|
|
@@ -305,9 +371,9 @@ export class FxFore extends HTMLElement {
|
|
|
305
371
|
_parseTargetList(raw) {
|
|
306
372
|
if (!raw) return [];
|
|
307
373
|
return raw
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
374
|
+
.split(/[\s,]+/)
|
|
375
|
+
.map(s => s.trim())
|
|
376
|
+
.filter(Boolean);
|
|
311
377
|
}
|
|
312
378
|
|
|
313
379
|
_findBySelector(sel) {
|
|
@@ -323,10 +389,10 @@ export class FxFore extends HTMLElement {
|
|
|
323
389
|
|
|
324
390
|
_isReadyTarget(el) {
|
|
325
391
|
return !!(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
392
|
+
el &&
|
|
393
|
+
(el.ready === true ||
|
|
394
|
+
(el.classList && el.classList.contains('fx-ready')) ||
|
|
395
|
+
(typeof el.hasAttribute === 'function' && el.hasAttribute('ready')))
|
|
330
396
|
);
|
|
331
397
|
}
|
|
332
398
|
|
|
@@ -343,7 +409,7 @@ export class FxFore extends HTMLElement {
|
|
|
343
409
|
if (waitForRaw) {
|
|
344
410
|
if (!this._warnedWaitForDeprecation) {
|
|
345
411
|
console.warn(
|
|
346
|
-
|
|
412
|
+
'[fx-fore] The "wait-for" attribute is deprecated. Use init-on="ready" init-on-target="..." instead.',
|
|
347
413
|
);
|
|
348
414
|
this._warnedWaitForDeprecation = true;
|
|
349
415
|
}
|
|
@@ -442,7 +508,7 @@ export class FxFore extends HTMLElement {
|
|
|
442
508
|
// Special: closest fx-fore
|
|
443
509
|
if (targetSpec === 'closest') {
|
|
444
510
|
const recheckFn =
|
|
445
|
-
|
|
511
|
+
event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
|
|
446
512
|
|
|
447
513
|
const matchesFn = ev => {
|
|
448
514
|
const t = ev.target;
|
|
@@ -456,7 +522,7 @@ export class FxFore extends HTMLElement {
|
|
|
456
522
|
const selector = targetSpec;
|
|
457
523
|
|
|
458
524
|
const recheckFn =
|
|
459
|
-
|
|
525
|
+
event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
|
|
460
526
|
|
|
461
527
|
if (typeof recheckFn === 'function' && recheckFn()) {
|
|
462
528
|
return Promise.resolve();
|
|
@@ -489,7 +555,7 @@ export class FxFore extends HTMLElement {
|
|
|
489
555
|
}
|
|
490
556
|
|
|
491
557
|
this._initGatesPromise = Promise.all(gates.map(g => this._waitForInitGate(g))).then(
|
|
492
|
-
|
|
558
|
+
() => undefined,
|
|
493
559
|
);
|
|
494
560
|
return this._initGatesPromise;
|
|
495
561
|
}
|
|
@@ -524,7 +590,7 @@ export class FxFore extends HTMLElement {
|
|
|
524
590
|
}
|
|
525
591
|
// Fallback for odd engines/polyfills
|
|
526
592
|
return (slotEl.assignedNodes({ flatten: true }) || []).filter(
|
|
527
|
-
|
|
593
|
+
n => n.nodeType === Node.ELEMENT_NODE,
|
|
528
594
|
);
|
|
529
595
|
};
|
|
530
596
|
|
|
@@ -542,8 +608,8 @@ export class FxFore extends HTMLElement {
|
|
|
542
608
|
}
|
|
543
609
|
if (!modelElement.inited) {
|
|
544
610
|
console.info(
|
|
545
|
-
|
|
546
|
-
|
|
611
|
+
`%cFore running ... ${this.id ? '#' + this.id : ''}`,
|
|
612
|
+
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
547
613
|
);
|
|
548
614
|
|
|
549
615
|
const variables = new Map();
|
|
@@ -562,7 +628,7 @@ export class FxFore extends HTMLElement {
|
|
|
562
628
|
await Promise.all(libs.map(l => l.readyPromise || Promise.resolve()));
|
|
563
629
|
|
|
564
630
|
await modelElement.modelConstruct();
|
|
565
|
-
console.log(
|
|
631
|
+
console.log('varbindings ', this._instanceVarBindings);
|
|
566
632
|
this._handleModelConstructDone();
|
|
567
633
|
}
|
|
568
634
|
|
|
@@ -570,7 +636,6 @@ export class FxFore extends HTMLElement {
|
|
|
570
636
|
this.inited = true;
|
|
571
637
|
};
|
|
572
638
|
|
|
573
|
-
|
|
574
639
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
575
640
|
if (oldValue === newValue) return;
|
|
576
641
|
|
|
@@ -611,7 +676,7 @@ export class FxFore extends HTMLElement {
|
|
|
611
676
|
|
|
612
677
|
connectedCallback() {
|
|
613
678
|
const modelElement = Array.from(this.children).find(
|
|
614
|
-
|
|
679
|
+
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
|
|
615
680
|
);
|
|
616
681
|
|
|
617
682
|
this.model = modelElement;
|
|
@@ -638,8 +703,8 @@ export class FxFore extends HTMLElement {
|
|
|
638
703
|
},true);
|
|
639
704
|
*/
|
|
640
705
|
this.ignoreExpressions = this.hasAttribute('ignore-expressions')
|
|
641
|
-
|
|
642
|
-
|
|
706
|
+
? this.getAttribute('ignore-expressions')
|
|
707
|
+
: null;
|
|
643
708
|
|
|
644
709
|
this.lazyRefresh = this.hasAttribute('refresh-on-view');
|
|
645
710
|
if (this.lazyRefresh) {
|
|
@@ -700,9 +765,9 @@ export class FxFore extends HTMLElement {
|
|
|
700
765
|
|
|
701
766
|
// Collect existing fx-var names at fx-fore scope (author-defined and previously generated)
|
|
702
767
|
const existingVars = new Set(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
768
|
+
Array.from(this.querySelectorAll(':scope > fx-var'))
|
|
769
|
+
.map(v => (v.getAttribute('name') || '').trim())
|
|
770
|
+
.filter(Boolean),
|
|
706
771
|
);
|
|
707
772
|
|
|
708
773
|
let defaultAssigned = false;
|
|
@@ -790,13 +855,13 @@ export class FxFore extends HTMLElement {
|
|
|
790
855
|
markAsClean() {
|
|
791
856
|
console.log('marking as clean', this);
|
|
792
857
|
this.addEventListener(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
858
|
+
'value-changed',
|
|
859
|
+
() => {
|
|
860
|
+
console.log('MARK as modified', this);
|
|
861
|
+
this.dirtyState = dirtyStates.DIRTY;
|
|
862
|
+
this.classList.toggle('fx-modified');
|
|
863
|
+
},
|
|
864
|
+
{ once: true },
|
|
800
865
|
);
|
|
801
866
|
this.dirtyState = dirtyStates.CLEAN;
|
|
802
867
|
this.classList.remove('fx-modified');
|
|
@@ -894,8 +959,11 @@ export class FxFore extends HTMLElement {
|
|
|
894
959
|
|
|
895
960
|
try {
|
|
896
961
|
if (force === true || this.initialRun) {
|
|
962
|
+
performance.mark('force-refresh-start');
|
|
897
963
|
console.log('🔄 🔴🔴🔴 ### full refresh() on ', this);
|
|
898
964
|
await Fore.refreshChildren(this, force);
|
|
965
|
+
performance.mark('force-refresh-end');
|
|
966
|
+
performance.measure('force-refresh', 'force-refresh-start', 'force-refresh-end');
|
|
899
967
|
} else {
|
|
900
968
|
await this._processBatchedNotifications();
|
|
901
969
|
}
|
|
@@ -912,9 +980,9 @@ export class FxFore extends HTMLElement {
|
|
|
912
980
|
this.style.visibility = 'visible';
|
|
913
981
|
|
|
914
982
|
console.info(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
983
|
+
`%c ✅ refresh-done on #${this.id}`,
|
|
984
|
+
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
985
|
+
this.getModel().modelItems,
|
|
918
986
|
);
|
|
919
987
|
|
|
920
988
|
Fore.dispatch(this, 'refresh-done', {});
|
|
@@ -954,7 +1022,7 @@ export class FxFore extends HTMLElement {
|
|
|
954
1022
|
*/
|
|
955
1023
|
_processBatchedNotifications() {
|
|
956
1024
|
if (this.batchedNotifications.size > 0) {
|
|
957
|
-
console.log(`🔄 🎯 ### processing ${
|
|
1025
|
+
console.log(`🔄 🎯 ### processing ${this.batchedNotifications.size} batched notifications`);
|
|
958
1026
|
console.log('🔄 🎯 ### processing ', Array.from(this.batchedNotifications));
|
|
959
1027
|
|
|
960
1028
|
// console.log(`🔍 Processing ${this.batchedNotifications.size} batched notifications`);
|
|
@@ -1016,7 +1084,7 @@ export class FxFore extends HTMLElement {
|
|
|
1016
1084
|
*/
|
|
1017
1085
|
_updateTemplateExpressions() {
|
|
1018
1086
|
const search =
|
|
1019
|
-
|
|
1087
|
+
"(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
|
|
1020
1088
|
|
|
1021
1089
|
const tmplExpressions = evaluateXPathToNodes(search, this, this);
|
|
1022
1090
|
// console.log('template expressions found ', tmplExpressions);
|
|
@@ -1027,7 +1095,7 @@ export class FxFore extends HTMLElement {
|
|
|
1027
1095
|
|
|
1028
1096
|
// console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
|
|
1029
1097
|
|
|
1030
|
-
if(!tmplExpressions) return;
|
|
1098
|
+
if (!tmplExpressions) return;
|
|
1031
1099
|
/*
|
|
1032
1100
|
storing expressions and their nodes for re-evaluation
|
|
1033
1101
|
*/
|
|
@@ -1104,7 +1172,7 @@ export class FxFore extends HTMLElement {
|
|
|
1104
1172
|
if (el && el.localName === 'fx-insert' && node.name === 'origin') {
|
|
1105
1173
|
const v = String(node.value ?? '').trim();
|
|
1106
1174
|
const isJsonLiteral =
|
|
1107
|
-
|
|
1175
|
+
(v.startsWith('{') && v.endsWith('}')) || (v.startsWith('[') && v.endsWith(']'));
|
|
1108
1176
|
if (isJsonLiteral) return;
|
|
1109
1177
|
}
|
|
1110
1178
|
}
|
|
@@ -1115,16 +1183,16 @@ export class FxFore extends HTMLElement {
|
|
|
1115
1183
|
// - fx-var scoping (in-scope variables)
|
|
1116
1184
|
// - context() in repeats (repeat item detection)
|
|
1117
1185
|
const definitionElement =
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1186
|
+
node.nodeType === Node.ATTRIBUTE_NODE
|
|
1187
|
+
? node.ownerElement
|
|
1188
|
+
: node.nodeType === Node.TEXT_NODE
|
|
1189
|
+
? node.parentElement || node.parentNode
|
|
1190
|
+
: node;
|
|
1123
1191
|
|
|
1124
1192
|
const formElement =
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1193
|
+
definitionElement && definitionElement.nodeType === Node.ELEMENT_NODE
|
|
1194
|
+
? definitionElement
|
|
1195
|
+
: this;
|
|
1128
1196
|
|
|
1129
1197
|
const replaced = String(expr ?? '').replace(/{[^}]*}/g, match => {
|
|
1130
1198
|
if (match === '{}') return match;
|
|
@@ -1159,7 +1227,7 @@ export class FxFore extends HTMLElement {
|
|
|
1159
1227
|
node.textContent = replaced;
|
|
1160
1228
|
}
|
|
1161
1229
|
}
|
|
1162
|
-
}
|
|
1230
|
+
} // eslint-disable-next-line class-methods-use-this
|
|
1163
1231
|
_getTemplateExpression(node) {
|
|
1164
1232
|
if (this.ignoredNodes) {
|
|
1165
1233
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
@@ -1208,17 +1276,17 @@ export class FxFore extends HTMLElement {
|
|
|
1208
1276
|
|
|
1209
1277
|
// ##### lazy creation should NOT take place if there's a parent Fore using shared instances
|
|
1210
1278
|
const parentFore =
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1279
|
+
this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
|
|
1280
|
+
? this.parentNode.closest('fx-fore')
|
|
1281
|
+
: null;
|
|
1214
1282
|
if (this.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
1215
1283
|
console.log('fragment', this.parentNode);
|
|
1216
1284
|
}
|
|
1217
1285
|
|
|
1218
1286
|
if (parentFore) {
|
|
1219
1287
|
const shared = parentFore
|
|
1220
|
-
|
|
1221
|
-
|
|
1288
|
+
.getModel()
|
|
1289
|
+
.instances.filter(shared => shared.hasAttribute('shared'));
|
|
1222
1290
|
if (shared.length !== 0) return;
|
|
1223
1291
|
}
|
|
1224
1292
|
|
|
@@ -1239,8 +1307,8 @@ export class FxFore extends HTMLElement {
|
|
|
1239
1307
|
}
|
|
1240
1308
|
} catch (e) {
|
|
1241
1309
|
console.warn(
|
|
1242
|
-
|
|
1243
|
-
|
|
1310
|
+
'lazyCreateInstance created an error attempting to create a document',
|
|
1311
|
+
e.message,
|
|
1244
1312
|
);
|
|
1245
1313
|
}
|
|
1246
1314
|
}
|
|
@@ -1297,8 +1365,8 @@ export class FxFore extends HTMLElement {
|
|
|
1297
1365
|
*/
|
|
1298
1366
|
async _initUI() {
|
|
1299
1367
|
console.info(
|
|
1300
|
-
|
|
1301
|
-
|
|
1368
|
+
`%cinitUI #${this.id}`,
|
|
1369
|
+
'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
1302
1370
|
);
|
|
1303
1371
|
|
|
1304
1372
|
const parentFore = this.closest('fx-fore');
|
|
@@ -1320,7 +1388,12 @@ export class FxFore extends HTMLElement {
|
|
|
1320
1388
|
|
|
1321
1389
|
// First refresh should be forced
|
|
1322
1390
|
if (this.createNodes) {
|
|
1391
|
+
performance.mark('initData-start');
|
|
1323
1392
|
this.initData();
|
|
1393
|
+
performance.mark('initData-end');
|
|
1394
|
+
|
|
1395
|
+
performance.measure('initData', 'initData-start', 'initData-end');
|
|
1396
|
+
|
|
1324
1397
|
const binds = this.getModel().querySelector('fx-bind');
|
|
1325
1398
|
if (binds) {
|
|
1326
1399
|
this.getModel().updateModel();
|
|
@@ -1338,13 +1411,12 @@ export class FxFore extends HTMLElement {
|
|
|
1338
1411
|
this.initialRun = false;
|
|
1339
1412
|
// console.log('### >>>>> dispatching ready >>>>>', this);
|
|
1340
1413
|
console.info(
|
|
1341
|
-
|
|
1342
|
-
|
|
1414
|
+
`%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
|
|
1415
|
+
'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
1343
1416
|
);
|
|
1344
1417
|
|
|
1345
1418
|
// console.log(`### <<<<< ${this.id} ready >>>>>`);
|
|
1346
1419
|
|
|
1347
|
-
// console.log('### modelItems: ', this.getModel().modelItems);
|
|
1348
1420
|
Fore.dispatch(this, 'ready', {});
|
|
1349
1421
|
// console.log('dataChanged', FxModel.dataChanged);
|
|
1350
1422
|
this.markAsClean();
|
|
@@ -1358,6 +1430,50 @@ export class FxFore extends HTMLElement {
|
|
|
1358
1430
|
e.stopPropagation();
|
|
1359
1431
|
e.dataTransfer.dropEffect = 'move';
|
|
1360
1432
|
});
|
|
1433
|
+
|
|
1434
|
+
// Run authoring checks after ready — they're diagnostic only and must not delay
|
|
1435
|
+
// the ready event or drag-listener registration (both of which tests depend on).
|
|
1436
|
+
try {
|
|
1437
|
+
await this._runAuthoringChecks();
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
console.warn('[fore] authoring check failed:', e.message);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
async _runAuthoringChecks() {
|
|
1444
|
+
if (this.hasAttribute('no-check')) return;
|
|
1445
|
+
if (new URLSearchParams(window.location.search).has('no-check')) return;
|
|
1446
|
+
|
|
1447
|
+
const { checkAuthoring } = await import('./authoring-check.js');
|
|
1448
|
+
const errors = checkAuthoring(this);
|
|
1449
|
+
if (errors.length) {
|
|
1450
|
+
this._showAuthoringErrors(errors);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
_showAuthoringErrors(errors) {
|
|
1455
|
+
const overlay = this.shadowRoot.getElementById('authoringErrors');
|
|
1456
|
+
const content = this.shadowRoot.getElementById('authoringErrorsContent');
|
|
1457
|
+
if (!overlay || !content) return;
|
|
1458
|
+
|
|
1459
|
+
const rows = errors
|
|
1460
|
+
.map(({ element, message }) => {
|
|
1461
|
+
const path = element
|
|
1462
|
+
? element.tagName.toLowerCase() + (element.id ? `#${element.id}` : '')
|
|
1463
|
+
: '?';
|
|
1464
|
+
const safeMsg = message.replace(/</g, '<').replace(/>/g, '>');
|
|
1465
|
+
const safePath = path.replace(/</g, '<').replace(/>/g, '>');
|
|
1466
|
+
return `<tr><td>${safePath}</td><td>${safeMsg}</td></tr>`;
|
|
1467
|
+
})
|
|
1468
|
+
.join('');
|
|
1469
|
+
|
|
1470
|
+
content.innerHTML = `
|
|
1471
|
+
<table>
|
|
1472
|
+
<thead><tr><th>Element</th><th>Problem</th></tr></thead>
|
|
1473
|
+
<tbody>${rows}</tbody>
|
|
1474
|
+
</table>`;
|
|
1475
|
+
|
|
1476
|
+
overlay.classList.add('show');
|
|
1361
1477
|
}
|
|
1362
1478
|
|
|
1363
1479
|
/**
|
|
@@ -1438,7 +1554,7 @@ export class FxFore extends HTMLElement {
|
|
|
1438
1554
|
const lastMatchingSibling = nodeset.reverse().find(node => parentElement.contains(node));
|
|
1439
1555
|
if (lastMatchingSibling) {
|
|
1440
1556
|
return lastMatchingSibling;
|
|
1441
|
-
|
|
1557
|
+
}
|
|
1442
1558
|
// Otherwise, just default to appending... If this runs multiple times for multiple nodes
|
|
1443
1559
|
// it's unexpected to always prepend and get the order of children reversed from the UI.
|
|
1444
1560
|
|
|
@@ -1465,25 +1581,187 @@ export class FxFore extends HTMLElement {
|
|
|
1465
1581
|
}
|
|
1466
1582
|
/**
|
|
1467
1583
|
* @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
|
|
1468
|
-
*
|
|
1469
1584
|
*/
|
|
1470
1585
|
initData(root = this) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1586
|
+
/**
|
|
1587
|
+
* @param {*} value
|
|
1588
|
+
* @returns {boolean}
|
|
1589
|
+
*/
|
|
1590
|
+
const isObjectLike = value =>
|
|
1591
|
+
value !== null && (typeof value === 'object' || typeof value === 'function');
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* @param {*} nodeset
|
|
1595
|
+
* @returns {boolean}
|
|
1596
|
+
*/
|
|
1597
|
+
const hasResolvedNodeset = nodeset => {
|
|
1598
|
+
if (nodeset == null) return false;
|
|
1599
|
+
|
|
1600
|
+
// Atomic values like strings/numbers/booleans are valid resolved XPath results
|
|
1601
|
+
if (!isObjectLike(nodeset)) return true;
|
|
1602
|
+
|
|
1603
|
+
if (Array.isArray(nodeset)) return nodeset.length > 0;
|
|
1604
|
+
|
|
1605
|
+
if (typeof nodeset.length === 'number' && !('nodeType' in nodeset)) {
|
|
1606
|
+
return nodeset.length > 0;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (typeof nodeset[Symbol.iterator] === 'function' && !('nodeType' in nodeset)) {
|
|
1610
|
+
for (const item of nodeset) {
|
|
1611
|
+
return item !== undefined;
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
return !!nodeset.nodeType;
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Only try create-nodes for path-like refs, not general expressions like sequences.
|
|
1621
|
+
* @param {string} ref
|
|
1622
|
+
* @returns {boolean}
|
|
1623
|
+
*/
|
|
1624
|
+
const isCreateNodesCandidate = ref => {
|
|
1625
|
+
const expr = String(ref || '').trim();
|
|
1626
|
+
if (!expr || expr === '.') return false;
|
|
1627
|
+
if (expr.startsWith('"') || expr.startsWith("'")) return false;
|
|
1628
|
+
|
|
1629
|
+
// Ignore only simple literal sequences like ('a', 'b', 'c') or (1, 2, 3).
|
|
1630
|
+
// Keep fx-repeat refs that are real path expressions or more complex XPath.
|
|
1631
|
+
const simpleSequencePattern =
|
|
1632
|
+
/^\(\s*(?:(?:'[^']*'|"[^"]*"|\d+(?:\.\d+)?|\.)(?:\s*,\s*(?:'[^']*'|"[^"]*"|\d+(?:\.\d+)?|\.))*)?\s*\)$/;
|
|
1633
|
+
if (simpleSequencePattern.test(expr)) return false;
|
|
1634
|
+
|
|
1635
|
+
return true;
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Detect whether a ref ends in an attribute step.
|
|
1640
|
+
* @param {string} ref
|
|
1641
|
+
* @returns {boolean}
|
|
1642
|
+
*/
|
|
1643
|
+
const isAttributeRef = ref => /(^|\/)\s*@/.test(String(ref || '').trim());
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Normalize a possibly sequence-like nodeset/context to a single DOM node.
|
|
1647
|
+
* @param {*} candidate
|
|
1648
|
+
* @returns {*}
|
|
1649
|
+
*/
|
|
1650
|
+
const firstNode = candidate => {
|
|
1651
|
+
if (!candidate) return null;
|
|
1652
|
+
if (!isObjectLike(candidate)) return null;
|
|
1653
|
+
if (candidate.nodeType) return candidate;
|
|
1654
|
+
|
|
1655
|
+
if (Array.isArray(candidate)) {
|
|
1656
|
+
return candidate.find(item => item && isObjectLike(item) && item.nodeType) || null;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (typeof candidate.length === 'number' && typeof candidate.item === 'function') {
|
|
1660
|
+
for (let i = 0; i < candidate.length; i += 1) {
|
|
1661
|
+
const item = candidate.item(i);
|
|
1662
|
+
if (item && isObjectLike(item) && item.nodeType) return item;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
return null;
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Detect XPath results that are sequences of atomic values rather than DOM nodes.
|
|
1671
|
+
* These are valid resolved results for repeats, but they must never trigger create-nodes.
|
|
1672
|
+
* @param {*} candidate
|
|
1673
|
+
* @returns {boolean}
|
|
1674
|
+
*/
|
|
1675
|
+
const isAtomicSequence = candidate => {
|
|
1676
|
+
if (!candidate) return false;
|
|
1677
|
+
|
|
1678
|
+
// A single primitive value is also atomic from our perspective
|
|
1679
|
+
if (!isObjectLike(candidate)) return true;
|
|
1680
|
+
|
|
1681
|
+
if (candidate.nodeType) return false;
|
|
1682
|
+
|
|
1683
|
+
if (Array.isArray(candidate)) {
|
|
1684
|
+
return (
|
|
1685
|
+
candidate.length > 0 &&
|
|
1686
|
+
!candidate.some(item => item && isObjectLike(item) && item.nodeType)
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (typeof candidate.length === 'number' && typeof candidate.item === 'function') {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (typeof candidate[Symbol.iterator] === 'function') {
|
|
1695
|
+
for (const item of candidate) {
|
|
1696
|
+
return !(item && isObjectLike(item) && item.nodeType);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
return false;
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Check whether a bound element is resolved after evalInContext.
|
|
1705
|
+
* Attribute refs often expose an empty string as nodeset, so use the model item node in that case.
|
|
1706
|
+
* @param {import('./ForeElementMixin.js').default} bound
|
|
1707
|
+
* @returns {boolean}
|
|
1708
|
+
*/
|
|
1709
|
+
const isResolvedBound = bound => {
|
|
1710
|
+
if (hasResolvedNodeset(bound.nodeset)) return true;
|
|
1711
|
+
if (bound.nodeName === 'FX-REPEAT' && isAtomicSequence(bound.nodeset)) return true;
|
|
1712
|
+
if (isAttributeRef(bound.ref)) {
|
|
1713
|
+
const modelItem = typeof bound.getModelItem === 'function' ? bound.getModelItem() : null;
|
|
1714
|
+
return !!modelItem?.node;
|
|
1715
|
+
}
|
|
1716
|
+
return false;
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Determine the best context node for lazy node creation.
|
|
1721
|
+
* Prefer the bound element's in-scope context, but fall back to a structural parent.
|
|
1722
|
+
* @param {import('./ForeElementMixin.js').default} bound
|
|
1723
|
+
* @param {*} fallback
|
|
1724
|
+
* @returns {*}
|
|
1725
|
+
*/
|
|
1726
|
+
const getCreationContext = (bound, fallback) => {
|
|
1727
|
+
const direct =
|
|
1728
|
+
typeof bound.getInScopeContext === 'function' ? firstNode(bound.getInScopeContext()) : null;
|
|
1729
|
+
if (direct) return direct;
|
|
1730
|
+
|
|
1731
|
+
const dotCtx = firstNode(getInScopeContext(bound, '.'));
|
|
1732
|
+
if (dotCtx) return dotCtx;
|
|
1733
|
+
|
|
1734
|
+
const refCtx = firstNode(getInScopeContext(bound, bound.ref));
|
|
1735
|
+
if (refCtx) return refCtx;
|
|
1736
|
+
|
|
1737
|
+
return firstNode(fallback);
|
|
1738
|
+
};
|
|
1474
1739
|
|
|
1475
1740
|
/**
|
|
1476
1741
|
* @type {import('./ForeElementMixin.js').default[]}
|
|
1477
1742
|
*/
|
|
1478
1743
|
const boundControls = Array.from(
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
)
|
|
1483
|
-
|
|
1744
|
+
root.querySelectorAll(
|
|
1745
|
+
'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
|
|
1746
|
+
),
|
|
1747
|
+
).filter(boundEl => {
|
|
1748
|
+
if (boundEl.nodeName !== 'FX-REPEAT') return true;
|
|
1749
|
+
|
|
1750
|
+
const repeatRef = String(boundEl.getAttribute('ref') || '').trim();
|
|
1751
|
+
|
|
1752
|
+
// Any repeat whose ref is a pure parenthesized expression is not a create-nodes candidate.
|
|
1753
|
+
// Example: ('a', 'b', 'c')
|
|
1754
|
+
if (repeatRef.startsWith('(')) return false;
|
|
1755
|
+
|
|
1756
|
+
return isCreateNodesCandidate(repeatRef);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
if (root.matches && root.matches('fx-repeatitem') && firstNode(root.nodeset)) {
|
|
1484
1760
|
boundControls.unshift(root);
|
|
1485
1761
|
}
|
|
1762
|
+
|
|
1486
1763
|
console.log('_initData', boundControls);
|
|
1764
|
+
|
|
1487
1765
|
for (let i = 0; i < boundControls.length; i++) {
|
|
1488
1766
|
const bound = boundControls[i];
|
|
1489
1767
|
|
|
@@ -1494,11 +1772,21 @@ export class FxFore extends HTMLElement {
|
|
|
1494
1772
|
// Repeat items are dumb. They do not respond to evalInContext
|
|
1495
1773
|
bound.evalInContext();
|
|
1496
1774
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1775
|
+
|
|
1776
|
+
if (bound.nodeName === 'FX-REPEAT' && isAtomicSequence(bound.nodeset)) {
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
if (isResolvedBound(bound)) {
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
if (!isCreateNodesCandidate(bound.ref)) {
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// Ignore bound elements in a different form. They will be taken care of in the other form.
|
|
1787
|
+
if (bound.closest('fx-fore') !== this) {
|
|
1499
1788
|
continue;
|
|
1500
1789
|
}
|
|
1501
|
-
// console.log('Node does not exists', bound.ref);
|
|
1502
1790
|
|
|
1503
1791
|
// We need to create that node!
|
|
1504
1792
|
const previousControl = boundControls[i - 1];
|
|
@@ -1506,26 +1794,16 @@ export class FxFore extends HTMLElement {
|
|
|
1506
1794
|
// Previous control can either be an ancestor of us, or a previous node, which can be a sibling, or a child of a sibling.
|
|
1507
1795
|
// First: parent
|
|
1508
1796
|
if (previousControl && previousControl.contains(bound)) {
|
|
1509
|
-
// Parent is here.
|
|
1510
|
-
// console.log('insert into', bound, previousControl);
|
|
1511
|
-
// console.log('insert into nodeset', bound.nodeset);
|
|
1512
1797
|
/**
|
|
1513
1798
|
* @type {ParentNode}
|
|
1514
1799
|
*/
|
|
1515
|
-
const parentNodeset =
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
// const parentModelItemNode = parentModelItem.node;
|
|
1800
|
+
const parentNodeset =
|
|
1801
|
+
firstNode(previousControl.nodeset) || firstNode(root.getModel().getDefaultContext());
|
|
1802
|
+
const creationContext = getCreationContext(bound, parentNodeset);
|
|
1519
1803
|
const ref = bound.ref;
|
|
1520
|
-
// const newElement = parentModelItemNode.ownerDocument.createElement(ref);
|
|
1521
|
-
// if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
|
|
1522
|
-
// console.log(`Node with ref "${ref}" already exists.`);
|
|
1523
|
-
// continue;
|
|
1524
|
-
// }
|
|
1525
1804
|
|
|
1526
|
-
const newNode = this._createNodes(ref, parentNodeset);
|
|
1527
|
-
if (!newNode) {
|
|
1528
|
-
// We could not make the node for some reason. Maybe it's something like `instance('XXX')`?
|
|
1805
|
+
const newNode = this._createNodes(ref, creationContext || parentNodeset);
|
|
1806
|
+
if (!newNode || !parentNodeset) {
|
|
1529
1807
|
continue;
|
|
1530
1808
|
}
|
|
1531
1809
|
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
|
|
@@ -1543,17 +1821,11 @@ export class FxFore extends HTMLElement {
|
|
|
1543
1821
|
// Do not try to get a bind for a nodeSET of a repeat. there are multiple.
|
|
1544
1822
|
bound.getModelItem().bind?.evalInContext();
|
|
1545
1823
|
}
|
|
1546
|
-
|
|
1547
|
-
// console.log('CREATED child', newElement);
|
|
1548
|
-
// console.log('new control evaluated to ', control.nodeset);
|
|
1549
|
-
// Done!
|
|
1550
1824
|
continue;
|
|
1551
1825
|
}
|
|
1552
|
-
|
|
1553
|
-
// console.log('control', control);
|
|
1826
|
+
|
|
1554
1827
|
// Is previousControl a sibling or a descendant of a logical sibling? Keep looking backwards until we share parents!
|
|
1555
|
-
|
|
1556
|
-
// console.log('ourParent', ourParent);
|
|
1828
|
+
const ourParent = XPathUtil.getParentBindingElement(bound);
|
|
1557
1829
|
let siblingControl = null;
|
|
1558
1830
|
|
|
1559
1831
|
for (let j = i - 1; j > 0; --j) {
|
|
@@ -1566,11 +1838,7 @@ export class FxFore extends HTMLElement {
|
|
|
1566
1838
|
break;
|
|
1567
1839
|
}
|
|
1568
1840
|
}
|
|
1569
|
-
|
|
1570
|
-
// console.log('No sibling found for', bound);
|
|
1571
|
-
}
|
|
1572
|
-
// console.log('sibling', siblingControl);
|
|
1573
|
-
// todo: review: should this not just be inscopeContext?
|
|
1841
|
+
|
|
1574
1842
|
let parentNodeset;
|
|
1575
1843
|
if (!ourParent || !ourParent.nodeset) {
|
|
1576
1844
|
/*
|
|
@@ -1579,13 +1847,13 @@ export class FxFore extends HTMLElement {
|
|
|
1579
1847
|
*/
|
|
1580
1848
|
parentNodeset = root.getModel().getDefaultContext();
|
|
1581
1849
|
} else {
|
|
1582
|
-
parentNodeset = ourParent.nodeset;
|
|
1850
|
+
parentNodeset = firstNode(ourParent.nodeset) || root.getModel().getDefaultContext();
|
|
1583
1851
|
}
|
|
1584
1852
|
const ref = bound.ref;
|
|
1853
|
+
const creationContext = getCreationContext(bound, parentNodeset);
|
|
1585
1854
|
|
|
1586
|
-
const newNode = this._createNodes(ref, parentNodeset);
|
|
1855
|
+
const newNode = this._createNodes(ref, creationContext || parentNodeset);
|
|
1587
1856
|
if (!newNode) {
|
|
1588
|
-
// We could not make the node for some reason. Maybe it's something like `instance('XXX')`?
|
|
1589
1857
|
continue;
|
|
1590
1858
|
}
|
|
1591
1859
|
|
|
@@ -1593,13 +1861,12 @@ export class FxFore extends HTMLElement {
|
|
|
1593
1861
|
parentNodeset.setAttributeNode(newNode);
|
|
1594
1862
|
} else {
|
|
1595
1863
|
let referenceNode = this._findReferenceNodeForNewElement(
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1864
|
+
newNode,
|
|
1865
|
+
parentNodeset,
|
|
1866
|
+
siblingControl,
|
|
1599
1867
|
);
|
|
1600
1868
|
|
|
1601
1869
|
if (referenceNode) {
|
|
1602
|
-
// console.log('insert after', referenceNode,newNode);
|
|
1603
1870
|
if (referenceNode.nodeType === Node.DOCUMENT_NODE) {
|
|
1604
1871
|
referenceNode.firstElementChild.append(newNode);
|
|
1605
1872
|
} else {
|
|
@@ -1610,50 +1877,182 @@ export class FxFore extends HTMLElement {
|
|
|
1610
1877
|
}
|
|
1611
1878
|
}
|
|
1612
1879
|
|
|
1613
|
-
/*
|
|
1614
|
-
console.log('control inscope', control.getInScopeContext());
|
|
1615
|
-
console.log('control ref', control.ref);
|
|
1616
|
-
console.log('control new element parent', newElement.parentNode.nodeName);
|
|
1617
|
-
*/
|
|
1618
|
-
|
|
1619
1880
|
bound.evalInContext();
|
|
1620
1881
|
bound.getModelItem().bind?.evalInContext();
|
|
1621
1882
|
|
|
1622
|
-
if (!bound
|
|
1623
|
-
|
|
1883
|
+
if (!isResolvedBound(bound)) {
|
|
1884
|
+
console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
|
|
1885
|
+
continue;
|
|
1624
1886
|
}
|
|
1625
|
-
// console.log('new control evaluated to ', control.nodeset);
|
|
1626
|
-
// console.log('CREATED sibling', newElement);
|
|
1627
1887
|
}
|
|
1628
|
-
// console.log('DATA', this.getModel().getDefaultContext());
|
|
1629
1888
|
}
|
|
1630
|
-
|
|
1631
1889
|
_createNodes(ref, referenceNode) {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
}
|
|
1640
|
-
console.log(`creating new node for ref: ${ref}`);
|
|
1641
|
-
*/
|
|
1642
|
-
if (/instance\([^\)]*\)/.test(ref)) {
|
|
1643
|
-
// This is an absolute path for some instance. Not supporteed for now
|
|
1890
|
+
if (!ref || !referenceNode) return null;
|
|
1891
|
+
|
|
1892
|
+
const xpath = String(ref).trim();
|
|
1893
|
+
if (!xpath || xpath === '.') return null;
|
|
1894
|
+
|
|
1895
|
+
if (/^instance\([^\)]*\)/.test(xpath)) {
|
|
1896
|
+
// This is an absolute path for some instance. Not supported for create-nodes here.
|
|
1644
1897
|
return null;
|
|
1645
1898
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1899
|
+
|
|
1900
|
+
const ownerDoc =
|
|
1901
|
+
referenceNode.nodeType === Node.DOCUMENT_NODE ? referenceNode : referenceNode.ownerDocument;
|
|
1902
|
+
|
|
1903
|
+
const baseElement =
|
|
1904
|
+
referenceNode.nodeType === Node.DOCUMENT_NODE
|
|
1905
|
+
? referenceNode.documentElement
|
|
1906
|
+
: referenceNode.nodeType === Node.ATTRIBUTE_NODE
|
|
1907
|
+
? referenceNode.ownerElement
|
|
1908
|
+
: referenceNode;
|
|
1909
|
+
|
|
1910
|
+
if (!ownerDoc) return null;
|
|
1911
|
+
|
|
1912
|
+
const baseNamespace = baseElement?.namespaceURI || null;
|
|
1913
|
+
const namespaceResolver = createNamespaceResolver(xpath, this);
|
|
1914
|
+
|
|
1915
|
+
const parseName = token => {
|
|
1916
|
+
const raw = token.trim();
|
|
1917
|
+
|
|
1918
|
+
if (raw.startsWith('@')) {
|
|
1919
|
+
const attrToken = raw.slice(1);
|
|
1920
|
+
if (attrToken.startsWith('*:')) {
|
|
1921
|
+
return { isAttribute: true, namespaceURI: null, localName: attrToken.substring(2) };
|
|
1922
|
+
}
|
|
1923
|
+
if (attrToken.includes(':')) {
|
|
1924
|
+
const [prefix, localName] = attrToken.split(':');
|
|
1925
|
+
return {
|
|
1926
|
+
isAttribute: true,
|
|
1927
|
+
namespaceURI: prefix === '*' ? null : namespaceResolver(prefix) || null,
|
|
1928
|
+
localName,
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
return { isAttribute: true, namespaceURI: null, localName: attrToken };
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (raw.startsWith('*:')) {
|
|
1935
|
+
return { isAttribute: false, namespaceURI: baseNamespace, localName: raw.substring(2) };
|
|
1936
|
+
}
|
|
1937
|
+
if (raw.includes(':')) {
|
|
1938
|
+
const [prefix, localName] = raw.split(':');
|
|
1939
|
+
return {
|
|
1940
|
+
isAttribute: false,
|
|
1941
|
+
namespaceURI: prefix === '*' ? baseNamespace : namespaceResolver(prefix) || baseNamespace,
|
|
1942
|
+
localName,
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
return { isAttribute: false, namespaceURI: baseNamespace, localName: raw };
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
const parseStep = step => {
|
|
1949
|
+
const trimmed = step.trim();
|
|
1950
|
+
const nameMatch = trimmed.match(/^([^\[]+)/);
|
|
1951
|
+
const token = nameMatch ? nameMatch[1].trim() : trimmed;
|
|
1952
|
+
const predicates = [];
|
|
1953
|
+
|
|
1954
|
+
const predicateRegex = /\[\s*@([^\]\s=]+)\s*=\s*(['"])(.*?)\2\s*\]/g;
|
|
1955
|
+
let match;
|
|
1956
|
+
while ((match = predicateRegex.exec(trimmed)) !== null) {
|
|
1957
|
+
predicates.push({ name: match[1], value: match[3] });
|
|
1958
|
+
}
|
|
1959
|
+
return { token, predicates };
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
const splitSteps = xpath => {
|
|
1963
|
+
/**
|
|
1964
|
+
* @type {string[]}
|
|
1965
|
+
*/
|
|
1966
|
+
const steps = [];
|
|
1967
|
+
let scratch = '';
|
|
1968
|
+
let isInPredicate = false;
|
|
1969
|
+
for (const char of xpath.split('')) {
|
|
1970
|
+
if (char === '[') {
|
|
1971
|
+
isInPredicate = true;
|
|
1972
|
+
scratch += char;
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
if (char === ']') {
|
|
1976
|
+
scratch += char;
|
|
1977
|
+
isInPredicate = false;
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
if (!isInPredicate) {
|
|
1981
|
+
// Just add to the scratch. Do not check for slashes within predicates
|
|
1982
|
+
if (char === '/') {
|
|
1983
|
+
// Consume this path step
|
|
1984
|
+
if (scratch) {
|
|
1985
|
+
steps.push(scratch);
|
|
1986
|
+
}
|
|
1987
|
+
scratch = '';
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
scratch += char;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (scratch) {
|
|
1995
|
+
// Flush it
|
|
1996
|
+
steps.push(scratch);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
return steps;
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
const steps = splitSteps(xpath)
|
|
2003
|
+
.map(step => step.trim())
|
|
2004
|
+
.filter(step => step && step !== '.');
|
|
2005
|
+
|
|
2006
|
+
if (!steps.length) return null;
|
|
2007
|
+
|
|
2008
|
+
let subtreeRoot = null;
|
|
2009
|
+
let current = null;
|
|
2010
|
+
|
|
2011
|
+
for (const rawStep of steps) {
|
|
2012
|
+
const { token, predicates } = parseStep(rawStep);
|
|
2013
|
+
if (!token || token === '.') {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const parsed = parseName(token);
|
|
2018
|
+
|
|
2019
|
+
if (parsed.isAttribute) {
|
|
2020
|
+
if (!current) {
|
|
2021
|
+
const attr = ownerDoc.createAttribute(parsed.localName);
|
|
2022
|
+
return attr;
|
|
2023
|
+
}
|
|
2024
|
+
current.setAttribute(parsed.localName, '');
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
if (!isValidName(parsed.localName)) {
|
|
2029
|
+
// This did not result in a valid name. Stop.
|
|
2030
|
+
console.warn(
|
|
2031
|
+
`Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`,
|
|
2032
|
+
);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const element = parsed.namespaceURI
|
|
2037
|
+
? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName)
|
|
2038
|
+
: ownerDoc.createElement(parsed.localName);
|
|
2039
|
+
|
|
2040
|
+
for (const predicate of predicates) {
|
|
2041
|
+
const attrName = predicate.name.includes(':')
|
|
2042
|
+
? predicate.name.split(':')[1]
|
|
2043
|
+
: predicate.name;
|
|
2044
|
+
element.setAttribute(attrName, predicate.value);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (!subtreeRoot) {
|
|
2048
|
+
subtreeRoot = element;
|
|
2049
|
+
} else {
|
|
2050
|
+
current.appendChild(element);
|
|
2051
|
+
}
|
|
2052
|
+
current = element;
|
|
1656
2053
|
}
|
|
2054
|
+
|
|
2055
|
+
return subtreeRoot;
|
|
1657
2056
|
}
|
|
1658
2057
|
|
|
1659
2058
|
_handleDragStart(event) {
|