@jinntec/fore 3.0.1 → 3.1.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 +0 -2
- package/dist/fore-dev.js +5019 -4180
- package/dist/fore.js +4767 -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 +488 -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,7 @@ 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
|
-
libs.map(l => (l.readyPromise ? l.readyPromise : Promise.resolve()))
|
|
27
|
-
);
|
|
29
|
+
await Promise.all(libs.map(l => (l.readyPromise ? l.readyPromise : Promise.resolve())));
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -222,8 +224,6 @@ export class FxFore extends HTMLElement {
|
|
|
222
224
|
right:0;
|
|
223
225
|
left:0;
|
|
224
226
|
height:40px;
|
|
225
|
-
border-radius: 5px;
|
|
226
|
-
|
|
227
227
|
}
|
|
228
228
|
.popup .close {
|
|
229
229
|
position: absolute;
|
|
@@ -248,6 +248,53 @@ export class FxFore extends HTMLElement {
|
|
|
248
248
|
.warning{
|
|
249
249
|
background:orange;
|
|
250
250
|
}
|
|
251
|
+
#authoringErrors {
|
|
252
|
+
z-index: 20;
|
|
253
|
+
}
|
|
254
|
+
#authoringErrors .popup {
|
|
255
|
+
width: 70%;
|
|
256
|
+
max-height: 80vh;
|
|
257
|
+
overflow:hidden;
|
|
258
|
+
}
|
|
259
|
+
#authoringErrors h2 {
|
|
260
|
+
background: #c62828;
|
|
261
|
+
color: white;
|
|
262
|
+
padding-left: 12px;
|
|
263
|
+
line-height: 40px;
|
|
264
|
+
font-size: 1rem;
|
|
265
|
+
}
|
|
266
|
+
#authoringErrors table {
|
|
267
|
+
width: 100%;
|
|
268
|
+
border-collapse: collapse;
|
|
269
|
+
font-size: 0.85rem;
|
|
270
|
+
}
|
|
271
|
+
#authoringErrors th {
|
|
272
|
+
text-align: left;
|
|
273
|
+
border-bottom: 2px solid #c62828;
|
|
274
|
+
padding: 4px 8px;
|
|
275
|
+
}
|
|
276
|
+
#authoringErrors td {
|
|
277
|
+
padding: 4px 8px;
|
|
278
|
+
border-bottom: 1px solid #ddd;
|
|
279
|
+
vertical-align: top;
|
|
280
|
+
}
|
|
281
|
+
#authoringErrors td:first-child {
|
|
282
|
+
color: #555;
|
|
283
|
+
font-family: monospace;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
}
|
|
286
|
+
#authoringErrors .ae-actions {
|
|
287
|
+
text-align: center;
|
|
288
|
+
margin-top: 12px;
|
|
289
|
+
}
|
|
290
|
+
#authoringErrors .ae-actions button {
|
|
291
|
+
padding: 6px 20px;
|
|
292
|
+
background: #c62828;
|
|
293
|
+
color: white;
|
|
294
|
+
border: none;
|
|
295
|
+
border-radius: 3px;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
}
|
|
251
298
|
`;
|
|
252
299
|
|
|
253
300
|
const html = `
|
|
@@ -265,6 +312,14 @@ export class FxFore extends HTMLElement {
|
|
|
265
312
|
<div id="messageContent"></div>
|
|
266
313
|
</div>
|
|
267
314
|
</div>
|
|
315
|
+
<div id="authoringErrors" class="overlay">
|
|
316
|
+
<div class="popup">
|
|
317
|
+
<h2>Authoring Errors</h2>
|
|
318
|
+
<a class="close" href="#" onclick="event.preventDefault();event.target.closest('.overlay').classList.remove('show')">×</a>
|
|
319
|
+
<div id="authoringErrorsContent" style="margin-top:48px;"></div>
|
|
320
|
+
<div class="ae-actions"><button onclick="this.closest('.overlay').classList.remove('show')">Dismiss</button></div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
268
323
|
<slot name="event"></slot>
|
|
269
324
|
`;
|
|
270
325
|
|
|
@@ -281,8 +336,8 @@ export class FxFore extends HTMLElement {
|
|
|
281
336
|
this._scanForNewTemplateExpressionsNextRefresh = false;
|
|
282
337
|
this.repeatsFromAttributesCreated = false;
|
|
283
338
|
this.validateOn = this.hasAttribute('validate-on')
|
|
284
|
-
|
|
285
|
-
|
|
339
|
+
? this.getAttribute('validate-on')
|
|
340
|
+
: 'update';
|
|
286
341
|
// this.mergePartial = this.hasAttribute('merge-partial')? true:false;
|
|
287
342
|
this.mergePartial = false;
|
|
288
343
|
this.createNodes = this.hasAttribute('create-nodes') ? true : false;
|
|
@@ -305,9 +360,9 @@ export class FxFore extends HTMLElement {
|
|
|
305
360
|
_parseTargetList(raw) {
|
|
306
361
|
if (!raw) return [];
|
|
307
362
|
return raw
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
363
|
+
.split(/[\s,]+/)
|
|
364
|
+
.map(s => s.trim())
|
|
365
|
+
.filter(Boolean);
|
|
311
366
|
}
|
|
312
367
|
|
|
313
368
|
_findBySelector(sel) {
|
|
@@ -323,10 +378,10 @@ export class FxFore extends HTMLElement {
|
|
|
323
378
|
|
|
324
379
|
_isReadyTarget(el) {
|
|
325
380
|
return !!(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
381
|
+
el &&
|
|
382
|
+
(el.ready === true ||
|
|
383
|
+
(el.classList && el.classList.contains('fx-ready')) ||
|
|
384
|
+
(typeof el.hasAttribute === 'function' && el.hasAttribute('ready')))
|
|
330
385
|
);
|
|
331
386
|
}
|
|
332
387
|
|
|
@@ -343,7 +398,7 @@ export class FxFore extends HTMLElement {
|
|
|
343
398
|
if (waitForRaw) {
|
|
344
399
|
if (!this._warnedWaitForDeprecation) {
|
|
345
400
|
console.warn(
|
|
346
|
-
|
|
401
|
+
'[fx-fore] The "wait-for" attribute is deprecated. Use init-on="ready" init-on-target="..." instead.',
|
|
347
402
|
);
|
|
348
403
|
this._warnedWaitForDeprecation = true;
|
|
349
404
|
}
|
|
@@ -442,7 +497,7 @@ export class FxFore extends HTMLElement {
|
|
|
442
497
|
// Special: closest fx-fore
|
|
443
498
|
if (targetSpec === 'closest') {
|
|
444
499
|
const recheckFn =
|
|
445
|
-
|
|
500
|
+
event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
|
|
446
501
|
|
|
447
502
|
const matchesFn = ev => {
|
|
448
503
|
const t = ev.target;
|
|
@@ -456,7 +511,7 @@ export class FxFore extends HTMLElement {
|
|
|
456
511
|
const selector = targetSpec;
|
|
457
512
|
|
|
458
513
|
const recheckFn =
|
|
459
|
-
|
|
514
|
+
event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
|
|
460
515
|
|
|
461
516
|
if (typeof recheckFn === 'function' && recheckFn()) {
|
|
462
517
|
return Promise.resolve();
|
|
@@ -489,7 +544,7 @@ export class FxFore extends HTMLElement {
|
|
|
489
544
|
}
|
|
490
545
|
|
|
491
546
|
this._initGatesPromise = Promise.all(gates.map(g => this._waitForInitGate(g))).then(
|
|
492
|
-
|
|
547
|
+
() => undefined,
|
|
493
548
|
);
|
|
494
549
|
return this._initGatesPromise;
|
|
495
550
|
}
|
|
@@ -524,7 +579,7 @@ export class FxFore extends HTMLElement {
|
|
|
524
579
|
}
|
|
525
580
|
// Fallback for odd engines/polyfills
|
|
526
581
|
return (slotEl.assignedNodes({ flatten: true }) || []).filter(
|
|
527
|
-
|
|
582
|
+
n => n.nodeType === Node.ELEMENT_NODE,
|
|
528
583
|
);
|
|
529
584
|
};
|
|
530
585
|
|
|
@@ -542,8 +597,8 @@ export class FxFore extends HTMLElement {
|
|
|
542
597
|
}
|
|
543
598
|
if (!modelElement.inited) {
|
|
544
599
|
console.info(
|
|
545
|
-
|
|
546
|
-
|
|
600
|
+
`%cFore running ... ${this.id ? '#' + this.id : ''}`,
|
|
601
|
+
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
547
602
|
);
|
|
548
603
|
|
|
549
604
|
const variables = new Map();
|
|
@@ -562,7 +617,7 @@ export class FxFore extends HTMLElement {
|
|
|
562
617
|
await Promise.all(libs.map(l => l.readyPromise || Promise.resolve()));
|
|
563
618
|
|
|
564
619
|
await modelElement.modelConstruct();
|
|
565
|
-
console.log(
|
|
620
|
+
console.log('varbindings ', this._instanceVarBindings);
|
|
566
621
|
this._handleModelConstructDone();
|
|
567
622
|
}
|
|
568
623
|
|
|
@@ -570,7 +625,6 @@ export class FxFore extends HTMLElement {
|
|
|
570
625
|
this.inited = true;
|
|
571
626
|
};
|
|
572
627
|
|
|
573
|
-
|
|
574
628
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
575
629
|
if (oldValue === newValue) return;
|
|
576
630
|
|
|
@@ -611,7 +665,7 @@ export class FxFore extends HTMLElement {
|
|
|
611
665
|
|
|
612
666
|
connectedCallback() {
|
|
613
667
|
const modelElement = Array.from(this.children).find(
|
|
614
|
-
|
|
668
|
+
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
|
|
615
669
|
);
|
|
616
670
|
|
|
617
671
|
this.model = modelElement;
|
|
@@ -638,8 +692,8 @@ export class FxFore extends HTMLElement {
|
|
|
638
692
|
},true);
|
|
639
693
|
*/
|
|
640
694
|
this.ignoreExpressions = this.hasAttribute('ignore-expressions')
|
|
641
|
-
|
|
642
|
-
|
|
695
|
+
? this.getAttribute('ignore-expressions')
|
|
696
|
+
: null;
|
|
643
697
|
|
|
644
698
|
this.lazyRefresh = this.hasAttribute('refresh-on-view');
|
|
645
699
|
if (this.lazyRefresh) {
|
|
@@ -700,9 +754,9 @@ export class FxFore extends HTMLElement {
|
|
|
700
754
|
|
|
701
755
|
// Collect existing fx-var names at fx-fore scope (author-defined and previously generated)
|
|
702
756
|
const existingVars = new Set(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
757
|
+
Array.from(this.querySelectorAll(':scope > fx-var'))
|
|
758
|
+
.map(v => (v.getAttribute('name') || '').trim())
|
|
759
|
+
.filter(Boolean),
|
|
706
760
|
);
|
|
707
761
|
|
|
708
762
|
let defaultAssigned = false;
|
|
@@ -790,13 +844,13 @@ export class FxFore extends HTMLElement {
|
|
|
790
844
|
markAsClean() {
|
|
791
845
|
console.log('marking as clean', this);
|
|
792
846
|
this.addEventListener(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
847
|
+
'value-changed',
|
|
848
|
+
() => {
|
|
849
|
+
console.log('MARK as modified', this);
|
|
850
|
+
this.dirtyState = dirtyStates.DIRTY;
|
|
851
|
+
this.classList.toggle('fx-modified');
|
|
852
|
+
},
|
|
853
|
+
{ once: true },
|
|
800
854
|
);
|
|
801
855
|
this.dirtyState = dirtyStates.CLEAN;
|
|
802
856
|
this.classList.remove('fx-modified');
|
|
@@ -894,8 +948,11 @@ export class FxFore extends HTMLElement {
|
|
|
894
948
|
|
|
895
949
|
try {
|
|
896
950
|
if (force === true || this.initialRun) {
|
|
951
|
+
performance.mark('force-refresh-start');
|
|
897
952
|
console.log('🔄 🔴🔴🔴 ### full refresh() on ', this);
|
|
898
953
|
await Fore.refreshChildren(this, force);
|
|
954
|
+
performance.mark('force-refresh-end');
|
|
955
|
+
performance.measure('force-refresh', 'force-refresh-start', 'force-refresh-end');
|
|
899
956
|
} else {
|
|
900
957
|
await this._processBatchedNotifications();
|
|
901
958
|
}
|
|
@@ -912,9 +969,9 @@ export class FxFore extends HTMLElement {
|
|
|
912
969
|
this.style.visibility = 'visible';
|
|
913
970
|
|
|
914
971
|
console.info(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
972
|
+
`%c ✅ refresh-done on #${this.id}`,
|
|
973
|
+
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
974
|
+
this.getModel().modelItems,
|
|
918
975
|
);
|
|
919
976
|
|
|
920
977
|
Fore.dispatch(this, 'refresh-done', {});
|
|
@@ -954,7 +1011,7 @@ export class FxFore extends HTMLElement {
|
|
|
954
1011
|
*/
|
|
955
1012
|
_processBatchedNotifications() {
|
|
956
1013
|
if (this.batchedNotifications.size > 0) {
|
|
957
|
-
console.log(`🔄 🎯 ### processing ${
|
|
1014
|
+
console.log(`🔄 🎯 ### processing ${this.batchedNotifications.size} batched notifications`);
|
|
958
1015
|
console.log('🔄 🎯 ### processing ', Array.from(this.batchedNotifications));
|
|
959
1016
|
|
|
960
1017
|
// console.log(`🔍 Processing ${this.batchedNotifications.size} batched notifications`);
|
|
@@ -1016,7 +1073,7 @@ export class FxFore extends HTMLElement {
|
|
|
1016
1073
|
*/
|
|
1017
1074
|
_updateTemplateExpressions() {
|
|
1018
1075
|
const search =
|
|
1019
|
-
|
|
1076
|
+
"(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
|
|
1020
1077
|
|
|
1021
1078
|
const tmplExpressions = evaluateXPathToNodes(search, this, this);
|
|
1022
1079
|
// console.log('template expressions found ', tmplExpressions);
|
|
@@ -1027,7 +1084,7 @@ export class FxFore extends HTMLElement {
|
|
|
1027
1084
|
|
|
1028
1085
|
// console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
|
|
1029
1086
|
|
|
1030
|
-
if(!tmplExpressions) return;
|
|
1087
|
+
if (!tmplExpressions) return;
|
|
1031
1088
|
/*
|
|
1032
1089
|
storing expressions and their nodes for re-evaluation
|
|
1033
1090
|
*/
|
|
@@ -1104,7 +1161,7 @@ export class FxFore extends HTMLElement {
|
|
|
1104
1161
|
if (el && el.localName === 'fx-insert' && node.name === 'origin') {
|
|
1105
1162
|
const v = String(node.value ?? '').trim();
|
|
1106
1163
|
const isJsonLiteral =
|
|
1107
|
-
|
|
1164
|
+
(v.startsWith('{') && v.endsWith('}')) || (v.startsWith('[') && v.endsWith(']'));
|
|
1108
1165
|
if (isJsonLiteral) return;
|
|
1109
1166
|
}
|
|
1110
1167
|
}
|
|
@@ -1115,16 +1172,16 @@ export class FxFore extends HTMLElement {
|
|
|
1115
1172
|
// - fx-var scoping (in-scope variables)
|
|
1116
1173
|
// - context() in repeats (repeat item detection)
|
|
1117
1174
|
const definitionElement =
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1175
|
+
node.nodeType === Node.ATTRIBUTE_NODE
|
|
1176
|
+
? node.ownerElement
|
|
1177
|
+
: node.nodeType === Node.TEXT_NODE
|
|
1178
|
+
? node.parentElement || node.parentNode
|
|
1179
|
+
: node;
|
|
1123
1180
|
|
|
1124
1181
|
const formElement =
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1182
|
+
definitionElement && definitionElement.nodeType === Node.ELEMENT_NODE
|
|
1183
|
+
? definitionElement
|
|
1184
|
+
: this;
|
|
1128
1185
|
|
|
1129
1186
|
const replaced = String(expr ?? '').replace(/{[^}]*}/g, match => {
|
|
1130
1187
|
if (match === '{}') return match;
|
|
@@ -1159,7 +1216,7 @@ export class FxFore extends HTMLElement {
|
|
|
1159
1216
|
node.textContent = replaced;
|
|
1160
1217
|
}
|
|
1161
1218
|
}
|
|
1162
|
-
}
|
|
1219
|
+
} // eslint-disable-next-line class-methods-use-this
|
|
1163
1220
|
_getTemplateExpression(node) {
|
|
1164
1221
|
if (this.ignoredNodes) {
|
|
1165
1222
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
@@ -1208,17 +1265,17 @@ export class FxFore extends HTMLElement {
|
|
|
1208
1265
|
|
|
1209
1266
|
// ##### lazy creation should NOT take place if there's a parent Fore using shared instances
|
|
1210
1267
|
const parentFore =
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1268
|
+
this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
|
|
1269
|
+
? this.parentNode.closest('fx-fore')
|
|
1270
|
+
: null;
|
|
1214
1271
|
if (this.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
1215
1272
|
console.log('fragment', this.parentNode);
|
|
1216
1273
|
}
|
|
1217
1274
|
|
|
1218
1275
|
if (parentFore) {
|
|
1219
1276
|
const shared = parentFore
|
|
1220
|
-
|
|
1221
|
-
|
|
1277
|
+
.getModel()
|
|
1278
|
+
.instances.filter(shared => shared.hasAttribute('shared'));
|
|
1222
1279
|
if (shared.length !== 0) return;
|
|
1223
1280
|
}
|
|
1224
1281
|
|
|
@@ -1239,8 +1296,8 @@ export class FxFore extends HTMLElement {
|
|
|
1239
1296
|
}
|
|
1240
1297
|
} catch (e) {
|
|
1241
1298
|
console.warn(
|
|
1242
|
-
|
|
1243
|
-
|
|
1299
|
+
'lazyCreateInstance created an error attempting to create a document',
|
|
1300
|
+
e.message,
|
|
1244
1301
|
);
|
|
1245
1302
|
}
|
|
1246
1303
|
}
|
|
@@ -1297,8 +1354,8 @@ export class FxFore extends HTMLElement {
|
|
|
1297
1354
|
*/
|
|
1298
1355
|
async _initUI() {
|
|
1299
1356
|
console.info(
|
|
1300
|
-
|
|
1301
|
-
|
|
1357
|
+
`%cinitUI #${this.id}`,
|
|
1358
|
+
'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
1302
1359
|
);
|
|
1303
1360
|
|
|
1304
1361
|
const parentFore = this.closest('fx-fore');
|
|
@@ -1320,7 +1377,12 @@ export class FxFore extends HTMLElement {
|
|
|
1320
1377
|
|
|
1321
1378
|
// First refresh should be forced
|
|
1322
1379
|
if (this.createNodes) {
|
|
1380
|
+
performance.mark('initData-start');
|
|
1323
1381
|
this.initData();
|
|
1382
|
+
performance.mark('initData-end');
|
|
1383
|
+
|
|
1384
|
+
performance.measure('initData', 'initData-start', 'initData-end');
|
|
1385
|
+
|
|
1324
1386
|
const binds = this.getModel().querySelector('fx-bind');
|
|
1325
1387
|
if (binds) {
|
|
1326
1388
|
this.getModel().updateModel();
|
|
@@ -1338,13 +1400,12 @@ export class FxFore extends HTMLElement {
|
|
|
1338
1400
|
this.initialRun = false;
|
|
1339
1401
|
// console.log('### >>>>> dispatching ready >>>>>', this);
|
|
1340
1402
|
console.info(
|
|
1341
|
-
|
|
1342
|
-
|
|
1403
|
+
`%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
|
|
1404
|
+
'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
1343
1405
|
);
|
|
1344
1406
|
|
|
1345
1407
|
// console.log(`### <<<<< ${this.id} ready >>>>>`);
|
|
1346
1408
|
|
|
1347
|
-
// console.log('### modelItems: ', this.getModel().modelItems);
|
|
1348
1409
|
Fore.dispatch(this, 'ready', {});
|
|
1349
1410
|
// console.log('dataChanged', FxModel.dataChanged);
|
|
1350
1411
|
this.markAsClean();
|
|
@@ -1358,6 +1419,50 @@ export class FxFore extends HTMLElement {
|
|
|
1358
1419
|
e.stopPropagation();
|
|
1359
1420
|
e.dataTransfer.dropEffect = 'move';
|
|
1360
1421
|
});
|
|
1422
|
+
|
|
1423
|
+
// Run authoring checks after ready — they're diagnostic only and must not delay
|
|
1424
|
+
// the ready event or drag-listener registration (both of which tests depend on).
|
|
1425
|
+
try {
|
|
1426
|
+
await this._runAuthoringChecks();
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
console.warn('[fore] authoring check failed:', e.message);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async _runAuthoringChecks() {
|
|
1433
|
+
if (this.hasAttribute('no-check')) return;
|
|
1434
|
+
if (new URLSearchParams(window.location.search).has('no-check')) return;
|
|
1435
|
+
|
|
1436
|
+
const { checkAuthoring } = await import('./authoring-check.js');
|
|
1437
|
+
const errors = checkAuthoring(this);
|
|
1438
|
+
if (errors.length) {
|
|
1439
|
+
this._showAuthoringErrors(errors);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
_showAuthoringErrors(errors) {
|
|
1444
|
+
const overlay = this.shadowRoot.getElementById('authoringErrors');
|
|
1445
|
+
const content = this.shadowRoot.getElementById('authoringErrorsContent');
|
|
1446
|
+
if (!overlay || !content) return;
|
|
1447
|
+
|
|
1448
|
+
const rows = errors
|
|
1449
|
+
.map(({ element, message }) => {
|
|
1450
|
+
const path = element
|
|
1451
|
+
? element.tagName.toLowerCase() + (element.id ? `#${element.id}` : '')
|
|
1452
|
+
: '?';
|
|
1453
|
+
const safeMsg = message.replace(/</g, '<').replace(/>/g, '>');
|
|
1454
|
+
const safePath = path.replace(/</g, '<').replace(/>/g, '>');
|
|
1455
|
+
return `<tr><td>${safePath}</td><td>${safeMsg}</td></tr>`;
|
|
1456
|
+
})
|
|
1457
|
+
.join('');
|
|
1458
|
+
|
|
1459
|
+
content.innerHTML = `
|
|
1460
|
+
<table>
|
|
1461
|
+
<thead><tr><th>Element</th><th>Problem</th></tr></thead>
|
|
1462
|
+
<tbody>${rows}</tbody>
|
|
1463
|
+
</table>`;
|
|
1464
|
+
|
|
1465
|
+
overlay.classList.add('show');
|
|
1361
1466
|
}
|
|
1362
1467
|
|
|
1363
1468
|
/**
|
|
@@ -1438,7 +1543,7 @@ export class FxFore extends HTMLElement {
|
|
|
1438
1543
|
const lastMatchingSibling = nodeset.reverse().find(node => parentElement.contains(node));
|
|
1439
1544
|
if (lastMatchingSibling) {
|
|
1440
1545
|
return lastMatchingSibling;
|
|
1441
|
-
|
|
1546
|
+
}
|
|
1442
1547
|
// Otherwise, just default to appending... If this runs multiple times for multiple nodes
|
|
1443
1548
|
// it's unexpected to always prepend and get the order of children reversed from the UI.
|
|
1444
1549
|
|
|
@@ -1465,25 +1570,187 @@ export class FxFore extends HTMLElement {
|
|
|
1465
1570
|
}
|
|
1466
1571
|
/**
|
|
1467
1572
|
* @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
|
|
1468
|
-
*
|
|
1469
1573
|
*/
|
|
1470
1574
|
initData(root = this) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1575
|
+
/**
|
|
1576
|
+
* @param {*} value
|
|
1577
|
+
* @returns {boolean}
|
|
1578
|
+
*/
|
|
1579
|
+
const isObjectLike = value =>
|
|
1580
|
+
value !== null && (typeof value === 'object' || typeof value === 'function');
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* @param {*} nodeset
|
|
1584
|
+
* @returns {boolean}
|
|
1585
|
+
*/
|
|
1586
|
+
const hasResolvedNodeset = nodeset => {
|
|
1587
|
+
if (nodeset == null) return false;
|
|
1588
|
+
|
|
1589
|
+
// Atomic values like strings/numbers/booleans are valid resolved XPath results
|
|
1590
|
+
if (!isObjectLike(nodeset)) return true;
|
|
1591
|
+
|
|
1592
|
+
if (Array.isArray(nodeset)) return nodeset.length > 0;
|
|
1593
|
+
|
|
1594
|
+
if (typeof nodeset.length === 'number' && !('nodeType' in nodeset)) {
|
|
1595
|
+
return nodeset.length > 0;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (typeof nodeset[Symbol.iterator] === 'function' && !('nodeType' in nodeset)) {
|
|
1599
|
+
for (const item of nodeset) {
|
|
1600
|
+
return item !== undefined;
|
|
1601
|
+
}
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return !!nodeset.nodeType;
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Only try create-nodes for path-like refs, not general expressions like sequences.
|
|
1610
|
+
* @param {string} ref
|
|
1611
|
+
* @returns {boolean}
|
|
1612
|
+
*/
|
|
1613
|
+
const isCreateNodesCandidate = ref => {
|
|
1614
|
+
const expr = String(ref || '').trim();
|
|
1615
|
+
if (!expr || expr === '.') return false;
|
|
1616
|
+
if (expr.startsWith('"') || expr.startsWith("'")) return false;
|
|
1617
|
+
|
|
1618
|
+
// Ignore only simple literal sequences like ('a', 'b', 'c') or (1, 2, 3).
|
|
1619
|
+
// Keep fx-repeat refs that are real path expressions or more complex XPath.
|
|
1620
|
+
const simpleSequencePattern =
|
|
1621
|
+
/^\(\s*(?:(?:'[^']*'|"[^"]*"|\d+(?:\.\d+)?|\.)(?:\s*,\s*(?:'[^']*'|"[^"]*"|\d+(?:\.\d+)?|\.))*)?\s*\)$/;
|
|
1622
|
+
if (simpleSequencePattern.test(expr)) return false;
|
|
1623
|
+
|
|
1624
|
+
return true;
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Detect whether a ref ends in an attribute step.
|
|
1629
|
+
* @param {string} ref
|
|
1630
|
+
* @returns {boolean}
|
|
1631
|
+
*/
|
|
1632
|
+
const isAttributeRef = ref => /(^|\/)\s*@/.test(String(ref || '').trim());
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Normalize a possibly sequence-like nodeset/context to a single DOM node.
|
|
1636
|
+
* @param {*} candidate
|
|
1637
|
+
* @returns {*}
|
|
1638
|
+
*/
|
|
1639
|
+
const firstNode = candidate => {
|
|
1640
|
+
if (!candidate) return null;
|
|
1641
|
+
if (!isObjectLike(candidate)) return null;
|
|
1642
|
+
if (candidate.nodeType) return candidate;
|
|
1643
|
+
|
|
1644
|
+
if (Array.isArray(candidate)) {
|
|
1645
|
+
return candidate.find(item => item && isObjectLike(item) && item.nodeType) || null;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (typeof candidate.length === 'number' && typeof candidate.item === 'function') {
|
|
1649
|
+
for (let i = 0; i < candidate.length; i += 1) {
|
|
1650
|
+
const item = candidate.item(i);
|
|
1651
|
+
if (item && isObjectLike(item) && item.nodeType) return item;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
return null;
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Detect XPath results that are sequences of atomic values rather than DOM nodes.
|
|
1660
|
+
* These are valid resolved results for repeats, but they must never trigger create-nodes.
|
|
1661
|
+
* @param {*} candidate
|
|
1662
|
+
* @returns {boolean}
|
|
1663
|
+
*/
|
|
1664
|
+
const isAtomicSequence = candidate => {
|
|
1665
|
+
if (!candidate) return false;
|
|
1666
|
+
|
|
1667
|
+
// A single primitive value is also atomic from our perspective
|
|
1668
|
+
if (!isObjectLike(candidate)) return true;
|
|
1669
|
+
|
|
1670
|
+
if (candidate.nodeType) return false;
|
|
1671
|
+
|
|
1672
|
+
if (Array.isArray(candidate)) {
|
|
1673
|
+
return (
|
|
1674
|
+
candidate.length > 0 &&
|
|
1675
|
+
!candidate.some(item => item && isObjectLike(item) && item.nodeType)
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (typeof candidate.length === 'number' && typeof candidate.item === 'function') {
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
if (typeof candidate[Symbol.iterator] === 'function') {
|
|
1684
|
+
for (const item of candidate) {
|
|
1685
|
+
return !(item && isObjectLike(item) && item.nodeType);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return false;
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Check whether a bound element is resolved after evalInContext.
|
|
1694
|
+
* Attribute refs often expose an empty string as nodeset, so use the model item node in that case.
|
|
1695
|
+
* @param {import('./ForeElementMixin.js').default} bound
|
|
1696
|
+
* @returns {boolean}
|
|
1697
|
+
*/
|
|
1698
|
+
const isResolvedBound = bound => {
|
|
1699
|
+
if (hasResolvedNodeset(bound.nodeset)) return true;
|
|
1700
|
+
if (bound.nodeName === 'FX-REPEAT' && isAtomicSequence(bound.nodeset)) return true;
|
|
1701
|
+
if (isAttributeRef(bound.ref)) {
|
|
1702
|
+
const modelItem = typeof bound.getModelItem === 'function' ? bound.getModelItem() : null;
|
|
1703
|
+
return !!modelItem?.node;
|
|
1704
|
+
}
|
|
1705
|
+
return false;
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Determine the best context node for lazy node creation.
|
|
1710
|
+
* Prefer the bound element's in-scope context, but fall back to a structural parent.
|
|
1711
|
+
* @param {import('./ForeElementMixin.js').default} bound
|
|
1712
|
+
* @param {*} fallback
|
|
1713
|
+
* @returns {*}
|
|
1714
|
+
*/
|
|
1715
|
+
const getCreationContext = (bound, fallback) => {
|
|
1716
|
+
const direct =
|
|
1717
|
+
typeof bound.getInScopeContext === 'function' ? firstNode(bound.getInScopeContext()) : null;
|
|
1718
|
+
if (direct) return direct;
|
|
1719
|
+
|
|
1720
|
+
const dotCtx = firstNode(getInScopeContext(bound, '.'));
|
|
1721
|
+
if (dotCtx) return dotCtx;
|
|
1722
|
+
|
|
1723
|
+
const refCtx = firstNode(getInScopeContext(bound, bound.ref));
|
|
1724
|
+
if (refCtx) return refCtx;
|
|
1725
|
+
|
|
1726
|
+
return firstNode(fallback);
|
|
1727
|
+
};
|
|
1474
1728
|
|
|
1475
1729
|
/**
|
|
1476
1730
|
* @type {import('./ForeElementMixin.js').default[]}
|
|
1477
1731
|
*/
|
|
1478
1732
|
const boundControls = Array.from(
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
)
|
|
1483
|
-
|
|
1733
|
+
root.querySelectorAll(
|
|
1734
|
+
'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
|
|
1735
|
+
),
|
|
1736
|
+
).filter(boundEl => {
|
|
1737
|
+
if (boundEl.nodeName !== 'FX-REPEAT') return true;
|
|
1738
|
+
|
|
1739
|
+
const repeatRef = String(boundEl.getAttribute('ref') || '').trim();
|
|
1740
|
+
|
|
1741
|
+
// Any repeat whose ref is a pure parenthesized expression is not a create-nodes candidate.
|
|
1742
|
+
// Example: ('a', 'b', 'c')
|
|
1743
|
+
if (repeatRef.startsWith('(')) return false;
|
|
1744
|
+
|
|
1745
|
+
return isCreateNodesCandidate(repeatRef);
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
if (root.matches && root.matches('fx-repeatitem') && firstNode(root.nodeset)) {
|
|
1484
1749
|
boundControls.unshift(root);
|
|
1485
1750
|
}
|
|
1751
|
+
|
|
1486
1752
|
console.log('_initData', boundControls);
|
|
1753
|
+
|
|
1487
1754
|
for (let i = 0; i < boundControls.length; i++) {
|
|
1488
1755
|
const bound = boundControls[i];
|
|
1489
1756
|
|
|
@@ -1494,11 +1761,21 @@ export class FxFore extends HTMLElement {
|
|
|
1494
1761
|
// Repeat items are dumb. They do not respond to evalInContext
|
|
1495
1762
|
bound.evalInContext();
|
|
1496
1763
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1764
|
+
|
|
1765
|
+
if (bound.nodeName === 'FX-REPEAT' && isAtomicSequence(bound.nodeset)) {
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (isResolvedBound(bound)) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (!isCreateNodesCandidate(bound.ref)) {
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// Ignore bound elements in a different form. They will be taken care of in the other form.
|
|
1776
|
+
if (bound.closest('fx-fore') !== this) {
|
|
1499
1777
|
continue;
|
|
1500
1778
|
}
|
|
1501
|
-
// console.log('Node does not exists', bound.ref);
|
|
1502
1779
|
|
|
1503
1780
|
// We need to create that node!
|
|
1504
1781
|
const previousControl = boundControls[i - 1];
|
|
@@ -1506,26 +1783,16 @@ export class FxFore extends HTMLElement {
|
|
|
1506
1783
|
// 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
1784
|
// First: parent
|
|
1508
1785
|
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
1786
|
/**
|
|
1513
1787
|
* @type {ParentNode}
|
|
1514
1788
|
*/
|
|
1515
|
-
const parentNodeset =
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
// const parentModelItemNode = parentModelItem.node;
|
|
1789
|
+
const parentNodeset =
|
|
1790
|
+
firstNode(previousControl.nodeset) || firstNode(root.getModel().getDefaultContext());
|
|
1791
|
+
const creationContext = getCreationContext(bound, parentNodeset);
|
|
1519
1792
|
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
1793
|
|
|
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')`?
|
|
1794
|
+
const newNode = this._createNodes(ref, creationContext || parentNodeset);
|
|
1795
|
+
if (!newNode || !parentNodeset) {
|
|
1529
1796
|
continue;
|
|
1530
1797
|
}
|
|
1531
1798
|
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
|
|
@@ -1543,17 +1810,11 @@ export class FxFore extends HTMLElement {
|
|
|
1543
1810
|
// Do not try to get a bind for a nodeSET of a repeat. there are multiple.
|
|
1544
1811
|
bound.getModelItem().bind?.evalInContext();
|
|
1545
1812
|
}
|
|
1546
|
-
|
|
1547
|
-
// console.log('CREATED child', newElement);
|
|
1548
|
-
// console.log('new control evaluated to ', control.nodeset);
|
|
1549
|
-
// Done!
|
|
1550
1813
|
continue;
|
|
1551
1814
|
}
|
|
1552
|
-
|
|
1553
|
-
// console.log('control', control);
|
|
1815
|
+
|
|
1554
1816
|
// Is previousControl a sibling or a descendant of a logical sibling? Keep looking backwards until we share parents!
|
|
1555
|
-
|
|
1556
|
-
// console.log('ourParent', ourParent);
|
|
1817
|
+
const ourParent = XPathUtil.getParentBindingElement(bound);
|
|
1557
1818
|
let siblingControl = null;
|
|
1558
1819
|
|
|
1559
1820
|
for (let j = i - 1; j > 0; --j) {
|
|
@@ -1566,11 +1827,7 @@ export class FxFore extends HTMLElement {
|
|
|
1566
1827
|
break;
|
|
1567
1828
|
}
|
|
1568
1829
|
}
|
|
1569
|
-
|
|
1570
|
-
// console.log('No sibling found for', bound);
|
|
1571
|
-
}
|
|
1572
|
-
// console.log('sibling', siblingControl);
|
|
1573
|
-
// todo: review: should this not just be inscopeContext?
|
|
1830
|
+
|
|
1574
1831
|
let parentNodeset;
|
|
1575
1832
|
if (!ourParent || !ourParent.nodeset) {
|
|
1576
1833
|
/*
|
|
@@ -1579,13 +1836,13 @@ export class FxFore extends HTMLElement {
|
|
|
1579
1836
|
*/
|
|
1580
1837
|
parentNodeset = root.getModel().getDefaultContext();
|
|
1581
1838
|
} else {
|
|
1582
|
-
parentNodeset = ourParent.nodeset;
|
|
1839
|
+
parentNodeset = firstNode(ourParent.nodeset) || root.getModel().getDefaultContext();
|
|
1583
1840
|
}
|
|
1584
1841
|
const ref = bound.ref;
|
|
1842
|
+
const creationContext = getCreationContext(bound, parentNodeset);
|
|
1585
1843
|
|
|
1586
|
-
const newNode = this._createNodes(ref, parentNodeset);
|
|
1844
|
+
const newNode = this._createNodes(ref, creationContext || parentNodeset);
|
|
1587
1845
|
if (!newNode) {
|
|
1588
|
-
// We could not make the node for some reason. Maybe it's something like `instance('XXX')`?
|
|
1589
1846
|
continue;
|
|
1590
1847
|
}
|
|
1591
1848
|
|
|
@@ -1593,13 +1850,12 @@ export class FxFore extends HTMLElement {
|
|
|
1593
1850
|
parentNodeset.setAttributeNode(newNode);
|
|
1594
1851
|
} else {
|
|
1595
1852
|
let referenceNode = this._findReferenceNodeForNewElement(
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1853
|
+
newNode,
|
|
1854
|
+
parentNodeset,
|
|
1855
|
+
siblingControl,
|
|
1599
1856
|
);
|
|
1600
1857
|
|
|
1601
1858
|
if (referenceNode) {
|
|
1602
|
-
// console.log('insert after', referenceNode,newNode);
|
|
1603
1859
|
if (referenceNode.nodeType === Node.DOCUMENT_NODE) {
|
|
1604
1860
|
referenceNode.firstElementChild.append(newNode);
|
|
1605
1861
|
} else {
|
|
@@ -1610,50 +1866,135 @@ export class FxFore extends HTMLElement {
|
|
|
1610
1866
|
}
|
|
1611
1867
|
}
|
|
1612
1868
|
|
|
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
1869
|
bound.evalInContext();
|
|
1620
1870
|
bound.getModelItem().bind?.evalInContext();
|
|
1621
1871
|
|
|
1622
|
-
if (!bound
|
|
1623
|
-
|
|
1872
|
+
if (!isResolvedBound(bound)) {
|
|
1873
|
+
console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
|
|
1874
|
+
continue;
|
|
1624
1875
|
}
|
|
1625
|
-
// console.log('new control evaluated to ', control.nodeset);
|
|
1626
|
-
// console.log('CREATED sibling', newElement);
|
|
1627
1876
|
}
|
|
1628
|
-
// console.log('DATA', this.getModel().getDefaultContext());
|
|
1629
1877
|
}
|
|
1630
|
-
|
|
1631
1878
|
_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
|
|
1879
|
+
if (!ref || !referenceNode) return null;
|
|
1880
|
+
|
|
1881
|
+
const xpath = String(ref).trim();
|
|
1882
|
+
if (!xpath || xpath === '.') return null;
|
|
1883
|
+
|
|
1884
|
+
if (/^instance\([^\)]*\)/.test(xpath)) {
|
|
1885
|
+
// This is an absolute path for some instance. Not supported for create-nodes here.
|
|
1644
1886
|
return null;
|
|
1645
1887
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1888
|
+
|
|
1889
|
+
const ownerDoc =
|
|
1890
|
+
referenceNode.nodeType === Node.DOCUMENT_NODE ? referenceNode : referenceNode.ownerDocument;
|
|
1891
|
+
|
|
1892
|
+
const baseElement =
|
|
1893
|
+
referenceNode.nodeType === Node.DOCUMENT_NODE
|
|
1894
|
+
? referenceNode.documentElement
|
|
1895
|
+
: referenceNode.nodeType === Node.ATTRIBUTE_NODE
|
|
1896
|
+
? referenceNode.ownerElement
|
|
1897
|
+
: referenceNode;
|
|
1898
|
+
|
|
1899
|
+
if (!ownerDoc) return null;
|
|
1900
|
+
|
|
1901
|
+
const baseNamespace = baseElement?.namespaceURI || null;
|
|
1902
|
+
const namespaceResolver = createNamespaceResolver(xpath, this);
|
|
1903
|
+
|
|
1904
|
+
const parseName = token => {
|
|
1905
|
+
const raw = token.trim();
|
|
1906
|
+
|
|
1907
|
+
if (raw.startsWith('@')) {
|
|
1908
|
+
const attrToken = raw.slice(1);
|
|
1909
|
+
if (attrToken.startsWith('*:')) {
|
|
1910
|
+
return { isAttribute: true, namespaceURI: null, localName: attrToken.substring(2) };
|
|
1911
|
+
}
|
|
1912
|
+
if (attrToken.includes(':')) {
|
|
1913
|
+
const [prefix, localName] = attrToken.split(':');
|
|
1914
|
+
return {
|
|
1915
|
+
isAttribute: true,
|
|
1916
|
+
namespaceURI: prefix === '*' ? null : namespaceResolver(prefix) || null,
|
|
1917
|
+
localName,
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
return { isAttribute: true, namespaceURI: null, localName: attrToken };
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
if (raw.startsWith('*:')) {
|
|
1924
|
+
return { isAttribute: false, namespaceURI: baseNamespace, localName: raw.substring(2) };
|
|
1925
|
+
}
|
|
1926
|
+
if (raw.includes(':')) {
|
|
1927
|
+
const [prefix, localName] = raw.split(':');
|
|
1928
|
+
return {
|
|
1929
|
+
isAttribute: false,
|
|
1930
|
+
namespaceURI: prefix === '*' ? baseNamespace : namespaceResolver(prefix) || baseNamespace,
|
|
1931
|
+
localName,
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
return { isAttribute: false, namespaceURI: baseNamespace, localName: raw };
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
const parseStep = step => {
|
|
1938
|
+
const trimmed = step.trim();
|
|
1939
|
+
const nameMatch = trimmed.match(/^([^\[]+)/);
|
|
1940
|
+
const token = nameMatch ? nameMatch[1].trim() : trimmed;
|
|
1941
|
+
const predicates = [];
|
|
1942
|
+
|
|
1943
|
+
const predicateRegex = /\[\s*@([^\]\s=]+)\s*=\s*(['"])(.*?)\2\s*\]/g;
|
|
1944
|
+
let match;
|
|
1945
|
+
while ((match = predicateRegex.exec(trimmed)) !== null) {
|
|
1946
|
+
predicates.push({ name: match[1], value: match[3] });
|
|
1947
|
+
}
|
|
1948
|
+
return { token, predicates };
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
const steps = xpath
|
|
1952
|
+
.split('/')
|
|
1953
|
+
.map(step => step.trim())
|
|
1954
|
+
.filter(step => step && step !== '.');
|
|
1955
|
+
|
|
1956
|
+
if (!steps.length) return null;
|
|
1957
|
+
|
|
1958
|
+
let subtreeRoot = null;
|
|
1959
|
+
let current = null;
|
|
1960
|
+
|
|
1961
|
+
for (const rawStep of steps) {
|
|
1962
|
+
const { token, predicates } = parseStep(rawStep);
|
|
1963
|
+
if (!token || token === '.') {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const parsed = parseName(token);
|
|
1968
|
+
|
|
1969
|
+
if (parsed.isAttribute) {
|
|
1970
|
+
if (!current) {
|
|
1971
|
+
const attr = ownerDoc.createAttribute(parsed.localName);
|
|
1972
|
+
return attr;
|
|
1973
|
+
}
|
|
1974
|
+
current.setAttribute(parsed.localName, '');
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
const element = parsed.namespaceURI
|
|
1979
|
+
? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName)
|
|
1980
|
+
: ownerDoc.createElement(parsed.localName);
|
|
1981
|
+
|
|
1982
|
+
for (const predicate of predicates) {
|
|
1983
|
+
const attrName = predicate.name.includes(':')
|
|
1984
|
+
? predicate.name.split(':')[1]
|
|
1985
|
+
: predicate.name;
|
|
1986
|
+
element.setAttribute(attrName, predicate.value);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (!subtreeRoot) {
|
|
1990
|
+
subtreeRoot = element;
|
|
1991
|
+
} else {
|
|
1992
|
+
current.appendChild(element);
|
|
1993
|
+
}
|
|
1994
|
+
current = element;
|
|
1656
1995
|
}
|
|
1996
|
+
|
|
1997
|
+
return subtreeRoot;
|
|
1657
1998
|
}
|
|
1658
1999
|
|
|
1659
2000
|
_handleDragStart(event) {
|