@jinntec/fore 2.6.0 → 2.7.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/dist/fore-dev.js +4470 -2643
- package/dist/fore.js +4415 -2620
- package/index.js +2 -0
- package/package.json +10 -4
- package/resources/fore.css +2 -6
- package/src/DependencyNotifyingDomFacade.js +27 -21
- package/src/ForeElementMixin.js +282 -277
- package/src/actions/abstract-action.js +7 -12
- package/src/actions/fx-append.js +23 -2
- package/src/actions/fx-call.js +2 -2
- package/src/actions/fx-delete.js +54 -29
- package/src/actions/fx-insert.js +23 -15
- package/src/actions/fx-refresh.js +15 -5
- package/src/actions/fx-replace.js +18 -3
- package/src/actions/fx-reset.js +6 -0
- package/src/actions/fx-send.js +10 -7
- package/src/actions/fx-setattribute.js +12 -0
- package/src/actions/fx-setfocus.js +11 -8
- package/src/actions/fx-setvalue.js +93 -93
- package/src/actions/fx-toggle.js +1 -1
- package/src/extract-predicate-deps.js +57 -0
- package/src/extractPredicateDependencies.js +36 -0
- package/src/fore.js +41 -16
- package/src/fx-bind.js +128 -23
- package/src/fx-connection.js +24 -7
- package/src/fx-fore.js +506 -259
- package/src/fx-instance.js +9 -11
- package/src/fx-model.js +154 -65
- package/src/fx-submission.js +19 -30
- package/src/fx-var.js +5 -0
- package/src/getInScopeContext.js +7 -8
- package/src/modelitem.js +247 -112
- package/src/tools/fx-action-log.js +21 -19
- package/src/tools/fx-log-settings.js +4 -2
- package/src/ui/TemplateExpression.js +12 -0
- package/src/ui/UIElement.js +125 -10
- package/src/ui/abstract-control.js +5 -0
- package/src/ui/fx-case.js +15 -3
- package/src/ui/fx-container.js +5 -0
- package/src/ui/fx-control-menu.js +23 -14
- package/src/ui/fx-control.js +55 -15
- package/src/ui/fx-items.js +10 -4
- package/src/ui/fx-repeat-attributes.js +111 -35
- package/src/ui/fx-repeat.js +332 -85
- package/src/ui/fx-repeat.updated.js +821 -0
- package/src/ui/fx-repeatitem.js +23 -20
- package/src/ui/fx-switch.js +5 -3
- package/src/ui/fx-upload.js +36 -40
- package/src/ui/repeat-base.js +532 -0
- package/src/withDraggability.js +8 -4
- package/src/xpath-evaluation.js +26 -8
- package/src/xpath-path.js +79 -0
- package/src/xpath-util.js +357 -289
package/src/fx-fore.js
CHANGED
|
@@ -2,16 +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 {
|
|
6
|
-
evaluateXPathToBoolean,
|
|
7
|
-
evaluateXPathToNodes,
|
|
8
|
-
evaluateXPathToFirstNode,
|
|
9
|
-
evaluateXPathToString,
|
|
10
|
-
} from './xpath-evaluation.js';
|
|
5
|
+
import { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
|
|
11
6
|
import getInScopeContext from './getInScopeContext.js';
|
|
12
7
|
import { XPathUtil } from './xpath-util.js';
|
|
13
8
|
import { FxRepeatAttributes } from './ui/fx-repeat-attributes.js';
|
|
14
|
-
import {
|
|
9
|
+
import { FxBind } from './fx-bind.js';
|
|
15
10
|
|
|
16
11
|
/**
|
|
17
12
|
* Makes the dirty state of the form.
|
|
@@ -111,7 +106,10 @@ export class FxFore extends HTMLElement {
|
|
|
111
106
|
super();
|
|
112
107
|
this.version = '[VI]Version: {version} - built on {date}[/VI]';
|
|
113
108
|
|
|
114
|
-
|
|
109
|
+
/**
|
|
110
|
+
* @type {import('./fx-model.js').FxModel}
|
|
111
|
+
*/
|
|
112
|
+
this.model = null;
|
|
115
113
|
this.inited = false;
|
|
116
114
|
// this.addEventListener('model-construct-done', this._handleModelConstructDone);
|
|
117
115
|
// todo: refactoring - these should rather go into connectedcallback
|
|
@@ -136,6 +134,15 @@ export class FxFore extends HTMLElement {
|
|
|
136
134
|
this.dirtyState = dirtyStates.CLEAN;
|
|
137
135
|
this.showConfirmation = false;
|
|
138
136
|
|
|
137
|
+
// Batching for observer notifications during refresh
|
|
138
|
+
this.isRefreshPhase = false;
|
|
139
|
+
/**
|
|
140
|
+
* The model items that will be updated next refresh
|
|
141
|
+
*
|
|
142
|
+
* @type {Set<ModelItem|import('./ui/UIElement.js').UIElement}
|
|
143
|
+
*/
|
|
144
|
+
this.batchedNotifications = new Set();
|
|
145
|
+
|
|
139
146
|
const style = `
|
|
140
147
|
:host {
|
|
141
148
|
display: block;
|
|
@@ -251,12 +258,195 @@ export class FxFore extends HTMLElement {
|
|
|
251
258
|
this.setAttribute('role', 'form'); // set aria role
|
|
252
259
|
}
|
|
253
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Resolve elements from the `wait-for` attribute.
|
|
263
|
+
* Supports comma-separated CSS selectors and the special value "closest".
|
|
264
|
+
*/
|
|
265
|
+
_resolveDependencies() {
|
|
266
|
+
const raw = this.getAttribute('wait-for');
|
|
267
|
+
if (!raw) return [];
|
|
268
|
+
const sels = raw
|
|
269
|
+
.split(',')
|
|
270
|
+
.map(s => s.trim())
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
|
|
273
|
+
const roots = [this.getRootNode?.() ?? document, document];
|
|
274
|
+
const out = [];
|
|
275
|
+
|
|
276
|
+
for (const sel of sels) {
|
|
277
|
+
let el = null;
|
|
278
|
+
|
|
279
|
+
if (sel === 'closest') {
|
|
280
|
+
el = this.closest('fx-fore');
|
|
281
|
+
} else {
|
|
282
|
+
for (const r of roots) {
|
|
283
|
+
if (r && 'querySelector' in r) {
|
|
284
|
+
el = r.querySelector(sel);
|
|
285
|
+
if (el) break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (el) out.push(el);
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Wait until all dependencies are ready (i.e., they set `ready = true`
|
|
296
|
+
* and dispatch the `ready` event).
|
|
297
|
+
*/
|
|
298
|
+
_whenDependenciesReady() {
|
|
299
|
+
const raw = this.getAttribute('wait-for');
|
|
300
|
+
if (!raw) return Promise.resolve();
|
|
301
|
+
|
|
302
|
+
const sels = raw
|
|
303
|
+
.split(',')
|
|
304
|
+
.map(s => s.trim())
|
|
305
|
+
.filter(Boolean);
|
|
306
|
+
const roots = [this.getRootNode?.() ?? document, document];
|
|
307
|
+
|
|
308
|
+
const query = sel => {
|
|
309
|
+
for (const r of roots) {
|
|
310
|
+
if (r && 'querySelector' in r) {
|
|
311
|
+
const el = r.querySelector(sel);
|
|
312
|
+
if (el) return el;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const isReadyNow = sel => {
|
|
319
|
+
if (sel === 'closest') {
|
|
320
|
+
const outer = this.closest('fx-fore');
|
|
321
|
+
return !!(outer && outer.ready === true);
|
|
322
|
+
}
|
|
323
|
+
const el = query(sel);
|
|
324
|
+
return !!(el && el.ready === true);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const waitOne = sel =>
|
|
328
|
+
new Promise(resolve => {
|
|
329
|
+
// fast path
|
|
330
|
+
if (isReadyNow(sel)) return resolve();
|
|
331
|
+
|
|
332
|
+
// robust path: listen at the document/root so replacement doesn't matter
|
|
333
|
+
const onReady = ev => {
|
|
334
|
+
const t = ev.target;
|
|
335
|
+
if (sel === 'closest') {
|
|
336
|
+
// outer fore becoming ready anywhere above us
|
|
337
|
+
if (t?.tagName === 'FX-FORE' && t.contains(this)) {
|
|
338
|
+
cleanup();
|
|
339
|
+
resolve();
|
|
340
|
+
}
|
|
341
|
+
} else if (t?.matches?.(sel)) {
|
|
342
|
+
cleanup();
|
|
343
|
+
resolve();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const root = document; // capture at doc to catch composed events
|
|
348
|
+
const cleanup = () => root.removeEventListener('ready', onReady, true);
|
|
349
|
+
|
|
350
|
+
root.addEventListener('ready', onReady, true);
|
|
351
|
+
|
|
352
|
+
// also re-check on DOM changes in case a ready fore is inserted without firing (paranoia)
|
|
353
|
+
const mo = new MutationObserver(() => {
|
|
354
|
+
if (isReadyNow(sel)) {
|
|
355
|
+
mo.disconnect();
|
|
356
|
+
cleanup();
|
|
357
|
+
resolve();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
return Promise.all(sels.map(waitOne));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_onSlotChange = async ev => {
|
|
367
|
+
// 1) Capture the slot element BEFORE any await
|
|
368
|
+
const slotEl = ev.currentTarget;
|
|
369
|
+
if (!(slotEl instanceof HTMLSlotElement)) return;
|
|
370
|
+
|
|
371
|
+
// avoid double init
|
|
372
|
+
if (this.inited) return;
|
|
373
|
+
|
|
374
|
+
// 2) Wait for dependencies if needed
|
|
375
|
+
if (this.hasAttribute('wait-for')) {
|
|
376
|
+
try {
|
|
377
|
+
await this._whenDependenciesReady();
|
|
378
|
+
} catch (e) {
|
|
379
|
+
console.warn('wait-for failed', e);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 3) Bail if we got disconnected/replaced while waiting
|
|
385
|
+
if (!this.isConnected) return;
|
|
386
|
+
|
|
387
|
+
if (this.ignoreExpressions) {
|
|
388
|
+
this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 4) Safely read assigned content
|
|
392
|
+
const getAssignedElements = () => {
|
|
393
|
+
if (typeof slotEl.assignedElements === 'function') {
|
|
394
|
+
return slotEl.assignedElements({ flatten: true });
|
|
395
|
+
}
|
|
396
|
+
// Fallback for odd engines/polyfills
|
|
397
|
+
return (slotEl.assignedNodes({ flatten: true }) || []).filter(
|
|
398
|
+
n => n.nodeType === Node.ELEMENT_NODE,
|
|
399
|
+
);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// SAFE: slotEl is the actual event source, not a fresh query
|
|
403
|
+
const children = slotEl.assignedElements({ flatten: true });
|
|
404
|
+
|
|
405
|
+
let modelElement = children.find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');
|
|
406
|
+
if (!modelElement) {
|
|
407
|
+
const generatedModel = document.createElement('fx-model');
|
|
408
|
+
this.appendChild(generatedModel);
|
|
409
|
+
modelElement = generatedModel;
|
|
410
|
+
// We are going to get a new slotchange event immediately, because we changed a slot.
|
|
411
|
+
// so cancel this one.
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!modelElement.inited) {
|
|
415
|
+
console.info(
|
|
416
|
+
`%cFore running ... ${this.id ? '#' + this.id : ''}`,
|
|
417
|
+
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
const variables = new Map();
|
|
421
|
+
(function registerVariables(node) {
|
|
422
|
+
for (const child of node.children) {
|
|
423
|
+
if ('setInScopeVariables' in child) {
|
|
424
|
+
child.setInScopeVariables(variables);
|
|
425
|
+
}
|
|
426
|
+
registerVariables(child);
|
|
427
|
+
}
|
|
428
|
+
})(this);
|
|
429
|
+
|
|
430
|
+
await modelElement.modelConstruct();
|
|
431
|
+
this._handleModelConstructDone();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this._createRepeatsFromAttributes();
|
|
435
|
+
this.inited = true;
|
|
436
|
+
};
|
|
437
|
+
|
|
254
438
|
connectedCallback() {
|
|
439
|
+
const modelElement = Array.from(this.children).find(
|
|
440
|
+
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
this.model = modelElement;
|
|
444
|
+
|
|
255
445
|
this.style.visibility = 'hidden';
|
|
256
446
|
console.time('init');
|
|
257
447
|
this.strict = !!this.hasAttribute('strict');
|
|
258
448
|
/*
|
|
259
|
-
document.
|
|
449
|
+
document.re('ready', (e) =>{
|
|
260
450
|
if(e.target !== this){
|
|
261
451
|
// e.preventDefault();
|
|
262
452
|
console.log('>>> e', e);
|
|
@@ -295,57 +485,11 @@ export class FxFore extends HTMLElement {
|
|
|
295
485
|
|
|
296
486
|
this._injectDevtools();
|
|
297
487
|
|
|
298
|
-
const slot = this.shadowRoot.querySelector('slot#default');
|
|
299
|
-
slot.addEventListener('slotchange', async event => {
|
|
300
|
-
// preliminary addition for auto-conversion of non-prefixed element into prefixed elements. See fore.js
|
|
301
|
-
// console.log(`### <<<<< slotchange on '${this.id}' >>>>>`);
|
|
302
|
-
if (this.inited) return;
|
|
303
|
-
if (this.hasAttribute('convert')) {
|
|
304
|
-
this.replaceWith(Fore.copyDom(this));
|
|
305
|
-
// Fore.copyDom(this);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
488
|
+
// const slot = this.shadowRoot.querySelector('slot#default');
|
|
308
489
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
490
|
+
const slot = this.shadowRoot?.querySelector('slot') || this.querySelector('slot');
|
|
491
|
+
if (slot) slot.addEventListener('slotchange', this._onSlotChange);
|
|
312
492
|
|
|
313
|
-
const children = event.target.assignedElements();
|
|
314
|
-
let modelElement = children.find(
|
|
315
|
-
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
|
|
316
|
-
);
|
|
317
|
-
if (!modelElement) {
|
|
318
|
-
const generatedModel = document.createElement('fx-model');
|
|
319
|
-
this.appendChild(generatedModel);
|
|
320
|
-
modelElement = generatedModel;
|
|
321
|
-
// We are going to get a new slotchange event immediately, because we changed a slot.
|
|
322
|
-
// so cancel this one.
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (!modelElement.inited) {
|
|
326
|
-
console.info(
|
|
327
|
-
`%cFore is processing fx-fore#${this.id}`,
|
|
328
|
-
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
const variables = new Map();
|
|
332
|
-
(function registerVariables(node) {
|
|
333
|
-
for (const child of node.children) {
|
|
334
|
-
if ('setInScopeVariables' in child) {
|
|
335
|
-
child.setInScopeVariables(variables);
|
|
336
|
-
}
|
|
337
|
-
registerVariables(child);
|
|
338
|
-
}
|
|
339
|
-
})(this);
|
|
340
|
-
|
|
341
|
-
await modelElement.modelConstruct();
|
|
342
|
-
this._handleModelConstructDone();
|
|
343
|
-
}
|
|
344
|
-
this.model = modelElement;
|
|
345
|
-
|
|
346
|
-
this._createRepeatsFromAttributes();
|
|
347
|
-
this.inited = true;
|
|
348
|
-
});
|
|
349
493
|
this.addEventListener('path-mutated', () => {
|
|
350
494
|
this.someInstanceDataStructureChanged = true;
|
|
351
495
|
});
|
|
@@ -377,19 +521,6 @@ export class FxFore extends HTMLElement {
|
|
|
377
521
|
}
|
|
378
522
|
}
|
|
379
523
|
|
|
380
|
-
/**
|
|
381
|
-
* Add a model item to the refresh list
|
|
382
|
-
*
|
|
383
|
-
* @param {import('./modelitem.js').ModelItem} modelItem
|
|
384
|
-
* @returns {void}
|
|
385
|
-
*/
|
|
386
|
-
addToRefresh(modelItem) {
|
|
387
|
-
const found = this.toRefresh.find(mi => mi.path === modelItem.path);
|
|
388
|
-
if (!found) {
|
|
389
|
-
this.toRefresh.push(modelItem);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
524
|
/**
|
|
394
525
|
* Signal something happened with an element with the given local name. This will be used in the
|
|
395
526
|
* next (non-forceful) refresh to detect whether a component (usually a repeat) should update
|
|
@@ -428,7 +559,14 @@ export class FxFore extends HTMLElement {
|
|
|
428
559
|
*/
|
|
429
560
|
async _loadFromSrc() {
|
|
430
561
|
// console.log('########## loading Fore from ', this.src, '##########');
|
|
431
|
-
|
|
562
|
+
if (this.hasAttribute('wait-for')) {
|
|
563
|
+
await this._whenDependenciesReady();
|
|
564
|
+
}
|
|
565
|
+
if(this.hasAttribute('selector')){
|
|
566
|
+
await Fore.loadForeFromSrc(this, this.src, this.getAttribute('selector'));
|
|
567
|
+
}else{
|
|
568
|
+
await Fore.loadForeFromSrc(this, this.src, 'fx-fore');
|
|
569
|
+
}
|
|
432
570
|
}
|
|
433
571
|
|
|
434
572
|
/**
|
|
@@ -444,6 +582,8 @@ export class FxFore extends HTMLElement {
|
|
|
444
582
|
const { target } = entry;
|
|
445
583
|
|
|
446
584
|
const fore = Fore.getFore(target);
|
|
585
|
+
// Skip if this is the initial run of the fore element
|
|
586
|
+
// This check prevents issues with nested fx-fore elements loaded via fx-control
|
|
447
587
|
if (fore.initialRun) return;
|
|
448
588
|
|
|
449
589
|
if (entry.isIntersecting) {
|
|
@@ -484,32 +624,6 @@ export class FxFore extends HTMLElement {
|
|
|
484
624
|
*/
|
|
485
625
|
}
|
|
486
626
|
|
|
487
|
-
/**
|
|
488
|
-
* refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
|
|
489
|
-
* the bound modelItem to the bound element.
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* force - boolean - if true will refresh all children disregarding toRefresh array
|
|
493
|
-
*
|
|
494
|
-
*/
|
|
495
|
-
async forceRefresh() {
|
|
496
|
-
// console.time('refresh');
|
|
497
|
-
// console.group('### forced refresh', this);
|
|
498
|
-
|
|
499
|
-
Fore.refreshChildren(this, true);
|
|
500
|
-
this._updateTemplateExpressions();
|
|
501
|
-
this._scanForNewTemplateExpressionsNextRefresh = false; // reset
|
|
502
|
-
this._processTemplateExpressions();
|
|
503
|
-
|
|
504
|
-
// console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
|
|
505
|
-
|
|
506
|
-
Fore.dispatch(this, 'refresh-done', {});
|
|
507
|
-
|
|
508
|
-
// console.groupEnd();
|
|
509
|
-
// console.timeEnd('refresh');
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// async refresh(force, changedPaths) {
|
|
513
627
|
/**
|
|
514
628
|
* @param {(boolean|{reason:'index-function'})} [force]fx-fore
|
|
515
629
|
*/
|
|
@@ -518,6 +632,7 @@ export class FxFore extends HTMLElement {
|
|
|
518
632
|
return;
|
|
519
633
|
}
|
|
520
634
|
|
|
635
|
+
/*
|
|
521
636
|
if (force !== true && this._localNamesWithChanges.size > 0) {
|
|
522
637
|
force = {
|
|
523
638
|
...(force || { reason: undefined }),
|
|
@@ -525,114 +640,135 @@ export class FxFore extends HTMLElement {
|
|
|
525
640
|
};
|
|
526
641
|
this._localNamesWithChanges.clear();
|
|
527
642
|
}
|
|
643
|
+
*/
|
|
528
644
|
|
|
529
645
|
this.isRefreshing = true;
|
|
646
|
+
this.isRefreshPhase = true;
|
|
530
647
|
|
|
531
648
|
// refresh () {
|
|
532
649
|
// ### refresh Fore UI elements
|
|
533
650
|
// if (!this.initialRun && this.toRefresh.length !== 0) {
|
|
534
651
|
// if (!this.initialRun && this.toRefresh.length !== 0) {
|
|
535
|
-
if (!force && !this.initialRun && this.toRefresh.length !== 0) {
|
|
536
|
-
|
|
652
|
+
// if (!force && !this.initialRun && this.toRefresh.length !== 0) {
|
|
653
|
+
if (force === true || this.initialRun) {
|
|
654
|
+
console.log('🔄 🔴🔴🔴 ### full refresh() on ', this);
|
|
655
|
+
Fore.refreshChildren(this, force);
|
|
537
656
|
} else {
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
Array.from(visited).forEach(v =>{
|
|
542
|
-
v.classList.remove('visited');
|
|
543
|
-
});
|
|
544
|
-
*/
|
|
545
|
-
|
|
546
|
-
if (this.inited) {
|
|
547
|
-
console.log(`### <<<<< refresh() on '${this.id}' >>>>>`);
|
|
548
|
-
|
|
549
|
-
Fore.refreshChildren(this, force);
|
|
550
|
-
}
|
|
551
|
-
// console.timeEnd('refreshChildren');
|
|
657
|
+
// Process all batched no tifications at the end of the refresh phase
|
|
658
|
+
console.log('🔄 🎯 ### processing batched notifications');
|
|
659
|
+
await this._processBatchedNotifications();
|
|
552
660
|
}
|
|
553
661
|
|
|
554
662
|
// ### refresh template expressions
|
|
555
|
-
if (force || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
|
|
663
|
+
if (force === true || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
|
|
556
664
|
this._updateTemplateExpressions();
|
|
557
665
|
this._scanForNewTemplateExpressionsNextRefresh = false; // reset
|
|
558
666
|
}
|
|
559
667
|
|
|
560
668
|
this._processTemplateExpressions();
|
|
561
669
|
|
|
670
|
+
this.isRefreshPhase = false;
|
|
671
|
+
|
|
562
672
|
// console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
|
|
563
673
|
// this.dispatchEvent(new CustomEvent('refresh-done'));
|
|
564
|
-
|
|
674
|
+
this.initialRun = false;
|
|
565
675
|
this.style.visibility = 'visible';
|
|
566
676
|
console.info(
|
|
567
|
-
`%
|
|
677
|
+
`%c ✅ refresh-done on #${this.id}`,
|
|
568
678
|
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
679
|
+
this.getModel().modelItems,
|
|
569
680
|
);
|
|
570
681
|
|
|
571
682
|
Fore.dispatch(this, 'refresh-done', {});
|
|
572
683
|
|
|
573
|
-
// this.isRefreshing = true;
|
|
574
|
-
// this.parentNode.closest('fx-fore')?.refresh(false, changedPaths);
|
|
575
|
-
|
|
576
684
|
const subFores = Array.from(this.querySelectorAll('fx-fore'));
|
|
577
685
|
/*
|
|
578
|
-
|
|
579
|
-
|
|
686
|
+
calling the parent to refresh causes errors and inconsistent state. Also it is questionable
|
|
687
|
+
if a child should actually interact with its parent in this way.
|
|
580
688
|
|
|
581
|
-
|
|
689
|
+
This only affects the refreshing NOT the data mutation itself which is happening as expected.
|
|
582
690
|
|
|
583
|
-
|
|
584
|
-
|
|
691
|
+
Current solution is that a child that wants the parent to refresh must do so by adding an additional
|
|
692
|
+
event handler that dispatches an event upwards and having a handler in the parent to refresh itself.
|
|
585
693
|
|
|
586
|
-
|
|
694
|
+
So refreshed propagate downwards but not upwards which is at least an option to consider.
|
|
587
695
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
696
|
+
if(this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE){
|
|
697
|
+
// await this.parentNode.closest('fx-fore')?.refresh(false);
|
|
698
|
+
}
|
|
591
699
|
*/
|
|
592
700
|
for (const subFore of subFores) {
|
|
593
701
|
// subFore.refresh(false, changedPaths);
|
|
594
702
|
if (subFore.ready) {
|
|
595
703
|
// Do an unconditional hard refresh: there might be changes that are relevant
|
|
704
|
+
// todo: investigate impact of observer architecture - do we really want to refresh all subfore elements with a hard refresh?
|
|
596
705
|
await subFore.refresh(true);
|
|
597
706
|
}
|
|
598
707
|
}
|
|
599
708
|
this.isRefreshing = false;
|
|
709
|
+
// Clear the batch
|
|
710
|
+
// this.batchedNotifications.clear();
|
|
600
711
|
}
|
|
601
712
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
this.
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
ctrl.refresh(force);
|
|
613
|
-
});
|
|
614
|
-
}
|
|
713
|
+
/**
|
|
714
|
+
* Add a ModelItem to the batch of notifications to be processed at the end of the refresh phase
|
|
715
|
+
* @param {ModelItem | import('./ui/UIElement.js').UIElement} item - The ModelItem or UI Element to add to the batch
|
|
716
|
+
*/
|
|
717
|
+
addToBatchedNotifications(item) {
|
|
718
|
+
if (!this.batchedNotifications.has(item)) {
|
|
719
|
+
// console.log('adding to batched notifications', item);
|
|
720
|
+
this.batchedNotifications.add(item);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
615
723
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
724
|
+
/**
|
|
725
|
+
* Process all batched notifications at the end of the refresh phase
|
|
726
|
+
*/
|
|
727
|
+
_processBatchedNotifications() {
|
|
728
|
+
if (this.batchedNotifications.size > 0) {
|
|
729
|
+
// console.log(`🔍 Processing ${this.batchedNotifications.size} batched notifications`);
|
|
730
|
+
|
|
731
|
+
// Process all batched notifications
|
|
732
|
+
this.batchedNotifications.forEach(entry => {
|
|
733
|
+
// console.log('batched update', entry);
|
|
734
|
+
// handle repeatitems created via data-ref
|
|
735
|
+
if (entry.classList && entry.classList.contains('fx-repeatitem')) {
|
|
736
|
+
Fore.refreshChildren(entry, true);
|
|
737
|
+
}
|
|
738
|
+
if (entry && typeof entry.refresh === 'function') {
|
|
739
|
+
// Entry is a Ui Element
|
|
740
|
+
// Force refresh for this whole subtree
|
|
741
|
+
const uiElement = /** @type {import('./ui/UIElement.js').UIElement} */ (entry);
|
|
742
|
+
if (!uiElement.ownerDocument.contains(uiElement)) {
|
|
743
|
+
// Something already removed this ui element. Skip.
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
uiElement.refresh(true);
|
|
747
|
+
}
|
|
748
|
+
const nonrelevant = Array.from(this.querySelectorAll('[nonrelevant]'));
|
|
749
|
+
// loop nonrelevant elements
|
|
750
|
+
if (nonrelevant) {
|
|
751
|
+
nonrelevant.forEach(entry => {
|
|
752
|
+
if (entry.refresh) {
|
|
753
|
+
entry.refresh();
|
|
754
|
+
}
|
|
630
755
|
});
|
|
631
|
-
needsRefresh = true;
|
|
632
756
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
757
|
+
if (entry.observers) {
|
|
758
|
+
// Item is a model item
|
|
759
|
+
entry.observers.forEach(observer => {
|
|
760
|
+
// console.log('🔍 processing observer', observer);
|
|
761
|
+
if (typeof observer.update === 'function') {
|
|
762
|
+
// console.log('updating observer', observer);
|
|
763
|
+
observer.update(entry);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Clear the batch
|
|
770
|
+
this.batchedNotifications.clear();
|
|
771
|
+
}
|
|
636
772
|
}
|
|
637
773
|
|
|
638
774
|
/**
|
|
@@ -645,7 +781,7 @@ export class FxFore extends HTMLElement {
|
|
|
645
781
|
*/
|
|
646
782
|
_updateTemplateExpressions() {
|
|
647
783
|
const search =
|
|
648
|
-
"(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::fx-model)]";
|
|
784
|
+
"(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
|
|
649
785
|
|
|
650
786
|
const tmplExpressions = evaluateXPathToNodes(search, this, this);
|
|
651
787
|
// console.log('template expressions found ', tmplExpressions);
|
|
@@ -657,8 +793,8 @@ export class FxFore extends HTMLElement {
|
|
|
657
793
|
// console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
|
|
658
794
|
|
|
659
795
|
/*
|
|
660
|
-
|
|
661
|
-
|
|
796
|
+
storing expressions and their nodes for re-evaluation
|
|
797
|
+
*/
|
|
662
798
|
Array.from(tmplExpressions).forEach(node => {
|
|
663
799
|
const ele = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentNode;
|
|
664
800
|
if (ele.closest('fx-fore') !== this) {
|
|
@@ -683,6 +819,7 @@ export class FxFore extends HTMLElement {
|
|
|
683
819
|
}
|
|
684
820
|
|
|
685
821
|
_processTemplateExpressions() {
|
|
822
|
+
console.log('processing template expressions ', this.storedTemplateExpressionByNode);
|
|
686
823
|
for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
|
|
687
824
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
688
825
|
// Attribute nodes are not contained by the document, but their owner elements are!
|
|
@@ -746,10 +883,11 @@ export class FxFore extends HTMLElement {
|
|
|
746
883
|
: this.getModel().getDefaultInstance();
|
|
747
884
|
|
|
748
885
|
try {
|
|
749
|
-
|
|
886
|
+
const result = evaluateXPathToString(naked, inscope, node, null, inst);
|
|
887
|
+
// console.log(`template expression result for ${naked}=${result}`);
|
|
888
|
+
return result;
|
|
750
889
|
} catch (error) {
|
|
751
890
|
console.warn('ignoring unparseable expr', error);
|
|
752
|
-
|
|
753
891
|
return match;
|
|
754
892
|
}
|
|
755
893
|
});
|
|
@@ -896,45 +1034,6 @@ export class FxFore extends HTMLElement {
|
|
|
896
1034
|
return parent;
|
|
897
1035
|
}
|
|
898
1036
|
|
|
899
|
-
/*
|
|
900
|
-
_createStep(){
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
*/
|
|
904
|
-
|
|
905
|
-
/*
|
|
906
|
-
_generateInstance(start, parent) {
|
|
907
|
-
if (start.hasAttribute('ref')) {
|
|
908
|
-
const ref = start.getAttribute('ref');
|
|
909
|
-
|
|
910
|
-
if(ref.includes('/')){
|
|
911
|
-
console.log('complex path to create ', ref);
|
|
912
|
-
const steps = ref.split('/');
|
|
913
|
-
steps.forEach(step => {
|
|
914
|
-
console.log('step ', step);
|
|
915
|
-
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// const generated = document.createElement(ref);
|
|
920
|
-
const generated = parent.ownerDocument.createElement(ref);
|
|
921
|
-
if (start.children.length === 0) {
|
|
922
|
-
generated.textContent = start.textContent;
|
|
923
|
-
}
|
|
924
|
-
parent.appendChild(generated);
|
|
925
|
-
parent = generated;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (start.hasChildNodes()) {
|
|
929
|
-
const list = start.children;
|
|
930
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
931
|
-
this._generateInstance(list[i], parent);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
return parent;
|
|
935
|
-
}
|
|
936
|
-
*/
|
|
937
|
-
|
|
938
1037
|
/**
|
|
939
1038
|
* Start the initialization of the UI by
|
|
940
1039
|
*
|
|
@@ -946,29 +1045,39 @@ export class FxFore extends HTMLElement {
|
|
|
946
1045
|
* @private
|
|
947
1046
|
*/
|
|
948
1047
|
async _initUI() {
|
|
949
|
-
// console.log('### _initUI()');
|
|
950
1048
|
console.info(
|
|
951
1049
|
`%cinitUI #${this.id}`,
|
|
952
1050
|
'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
953
1051
|
);
|
|
954
1052
|
|
|
955
|
-
|
|
1053
|
+
const parentFore = this.closest('fx-fore');
|
|
1054
|
+
if (parentFore) {
|
|
1055
|
+
this.initialRun = false;
|
|
1056
|
+
} else {
|
|
1057
|
+
if (!this.initialRun) return;
|
|
1058
|
+
}
|
|
956
1059
|
this.classList.add('initialRun');
|
|
957
1060
|
await this._lazyCreateInstance();
|
|
958
1061
|
|
|
959
1062
|
/*
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1063
|
+
const options = {
|
|
1064
|
+
root: null,
|
|
1065
|
+
rootMargin: '0px',
|
|
1066
|
+
threshold: 0.3,
|
|
1067
|
+
};
|
|
1068
|
+
*/
|
|
966
1069
|
|
|
967
1070
|
// First refresh should be forced
|
|
968
1071
|
if (this.createNodes) {
|
|
969
1072
|
this.initData();
|
|
1073
|
+
const binds = this.getModel().querySelector('fx-bind');
|
|
1074
|
+
if (binds) {
|
|
1075
|
+
this.getModel().updateModel();
|
|
1076
|
+
}
|
|
970
1077
|
}
|
|
1078
|
+
// await this.forceRefresh();
|
|
971
1079
|
await this.refresh(true);
|
|
1080
|
+
// await Fore.initUI(this);
|
|
972
1081
|
|
|
973
1082
|
// this.style.display='block'
|
|
974
1083
|
this.classList.add('fx-ready');
|
|
@@ -978,8 +1087,8 @@ export class FxFore extends HTMLElement {
|
|
|
978
1087
|
this.initialRun = false;
|
|
979
1088
|
// console.log('### >>>>> dispatching ready >>>>>', this);
|
|
980
1089
|
console.info(
|
|
981
|
-
`%c
|
|
982
|
-
'background:
|
|
1090
|
+
`%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
|
|
1091
|
+
'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
983
1092
|
);
|
|
984
1093
|
|
|
985
1094
|
// console.log(`### <<<<< ${this.id} ready >>>>>`);
|
|
@@ -987,7 +1096,6 @@ export class FxFore extends HTMLElement {
|
|
|
987
1096
|
// console.log('### modelItems: ', this.getModel().modelItems);
|
|
988
1097
|
Fore.dispatch(this, 'ready', {});
|
|
989
1098
|
// console.log('dataChanged', FxModel.dataChanged);
|
|
990
|
-
console.timeEnd('init');
|
|
991
1099
|
|
|
992
1100
|
this.addEventListener('dragstart', this._handleDragStart);
|
|
993
1101
|
// this.addEventListener('dragend', this._handleDragEnd);
|
|
@@ -1000,24 +1108,131 @@ export class FxFore extends HTMLElement {
|
|
|
1000
1108
|
});
|
|
1001
1109
|
}
|
|
1002
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* @summary
|
|
1113
|
+
* Find the reference node (the future previous sibling) for a newly created element.
|
|
1114
|
+
*
|
|
1115
|
+
* @description This works in two passes: if there is a bind available for both the parent and the
|
|
1116
|
+
* child, it determines where to insert based on those binds: after an element matching the previous bind in document order, before the next sibling of that one cause `insertBefore` is easier .
|
|
1117
|
+
*
|
|
1118
|
+
* For example, take this structure:
|
|
1119
|
+
* ```html
|
|
1120
|
+
* <fx-bind ref="root">
|
|
1121
|
+
* <fx-bind ref="a" />
|
|
1122
|
+
* <fx-bind ref="b" />
|
|
1123
|
+
* <fx-bind ref="c" />
|
|
1124
|
+
* </fx-bind>
|
|
1125
|
+
* ```
|
|
1126
|
+
* Inserting a `<b/>`, it will be inserted before a `<c/>`, or at the end. Whatever comes after the `<a/>`.
|
|
1127
|
+
*
|
|
1128
|
+
* If there are no binds, the previous bound element will be used to determine the location.
|
|
1129
|
+
* @private
|
|
1130
|
+
*
|
|
1131
|
+
* @param {Element} newElement - The newly created element
|
|
1132
|
+
* @param {ParentNode} parentElement - The parent under which the element will be inserted
|
|
1133
|
+
* @param {import('./ForeElementMixin.js').default} previousControl - The previous control. Will
|
|
1134
|
+
* be used to determine a fallback to snert the element under if there are no binds for the parent
|
|
1135
|
+
*
|
|
1136
|
+
* @returns {ChildNode}
|
|
1137
|
+
*/
|
|
1138
|
+
_findReferenceNodeForNewElement(newElement, parentElement, previousControl) {
|
|
1139
|
+
const bindForElement = this.model.getModelItem(parentElement)?.bind;
|
|
1140
|
+
if (!bindForElement) {
|
|
1141
|
+
// Parent is unbound. No clue what to do with this. Insert based on previous control
|
|
1142
|
+
let referenceNode = previousControl?.getModelItem()?.node;
|
|
1143
|
+
// We know which node to insert this new element to, but it might be a descendant of a child
|
|
1144
|
+
// of the actual parent. Walk up until we have a reference under our parent
|
|
1145
|
+
while (referenceNode?.parentNode && referenceNode?.parentNode !== parentElement) {
|
|
1146
|
+
referenceNode = referenceNode.parentNode;
|
|
1147
|
+
}
|
|
1148
|
+
if (referenceNode?.nodeType === Node.ATTRIBUTE_NODE) {
|
|
1149
|
+
// Insert the new node at the start: the previous control was an attribute
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
return referenceNode;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Temporarily insert the new element under the parent to see which XPath will match
|
|
1156
|
+
try {
|
|
1157
|
+
parentElement.appendChild(newElement);
|
|
1158
|
+
|
|
1159
|
+
const bindForElement = this.model.getBindForElement(newElement);
|
|
1160
|
+
if (bindForElement) {
|
|
1161
|
+
// There is a bind for this element! Insert the new element after the last element that
|
|
1162
|
+
// matched in the preceding fx-bind
|
|
1163
|
+
|
|
1164
|
+
/*
|
|
1165
|
+
* Assumes a bind structure like this:
|
|
1166
|
+
*
|
|
1167
|
+
* ```xml
|
|
1168
|
+
* <fx-bind ref="root">
|
|
1169
|
+
* <fx-bind ref="a" />
|
|
1170
|
+
* <fx-bind ref="b" />
|
|
1171
|
+
* </fx-bind>
|
|
1172
|
+
* ```
|
|
1173
|
+
*
|
|
1174
|
+
* It will then attempt to keep all `b` elements after all `a` elements.
|
|
1175
|
+
*/
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* @type {FxBind}
|
|
1179
|
+
*/
|
|
1180
|
+
const previousBind = bindForElement.previousElementSibling;
|
|
1181
|
+
if (previousBind) {
|
|
1182
|
+
/**
|
|
1183
|
+
* @type ChildNode[]}
|
|
1184
|
+
*/
|
|
1185
|
+
const nodeset = previousBind.nodeset;
|
|
1186
|
+
const lastMatchingSibling = nodeset.reverse().find(node => parentElement.contains(node));
|
|
1187
|
+
if (lastMatchingSibling) {
|
|
1188
|
+
return lastMatchingSibling;
|
|
1189
|
+
}
|
|
1190
|
+
// Otherwise, just default to appending... If this runs multiple times for multiple nodes
|
|
1191
|
+
// it's unexpected to always prepend and get the order of children reversed from the UI.
|
|
1192
|
+
|
|
1193
|
+
// Do not fall back on the UI here, just keep it predictable if binds are in play
|
|
1194
|
+
return parentElement.lastElementChild;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
} finally {
|
|
1198
|
+
newElement.remove();
|
|
1199
|
+
}
|
|
1200
|
+
// No clue. Insert based on previous control. We know which node to insert this new element
|
|
1201
|
+
// into, but it might be a descendant of a child of the actual parent. Walk up until we have a
|
|
1202
|
+
// reference under our parent
|
|
1203
|
+
let referenceNode = previousControl?.getModelItem()?.node;
|
|
1204
|
+
while (referenceNode?.parentNode && referenceNode?.parentNode !== parentElement) {
|
|
1205
|
+
referenceNode = referenceNode.parentNode;
|
|
1206
|
+
}
|
|
1207
|
+
if (referenceNode?.nodeType === Node.ATTRIBUTE_NODE) {
|
|
1208
|
+
// Insert the new node at the start: the previous control was an attribute
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
// Insert after the previous control
|
|
1212
|
+
return referenceNode;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1003
1215
|
/**
|
|
1004
1216
|
* @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
|
|
1005
1217
|
*
|
|
1006
1218
|
*/
|
|
1007
1219
|
initData(root = this) {
|
|
1008
1220
|
// const created = new Promise(resolve => {
|
|
1009
|
-
|
|
1221
|
+
console.log('INIT');
|
|
1010
1222
|
// const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
|
|
1011
1223
|
|
|
1224
|
+
/**
|
|
1225
|
+
* @type {import('./ForeElementMixin.js').default[]}
|
|
1226
|
+
*/
|
|
1012
1227
|
const boundControls = Array.from(
|
|
1013
1228
|
root.querySelectorAll(
|
|
1014
1229
|
'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
|
|
1015
1230
|
),
|
|
1016
1231
|
);
|
|
1017
|
-
if (root.matches('fx-repeatitem')) {
|
|
1232
|
+
if (root.matches && root.matches('fx-repeatitem')) {
|
|
1018
1233
|
boundControls.unshift(root);
|
|
1019
1234
|
}
|
|
1020
|
-
|
|
1235
|
+
console.log('_initData', boundControls);
|
|
1021
1236
|
for (let i = 0; i < boundControls.length; i++) {
|
|
1022
1237
|
const bound = boundControls[i];
|
|
1023
1238
|
|
|
@@ -1028,38 +1243,53 @@ export class FxFore extends HTMLElement {
|
|
|
1028
1243
|
// Repeat items are dumb. They do not respond to evalInContext
|
|
1029
1244
|
bound.evalInContext();
|
|
1030
1245
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
// console.log('Node exists', control.nodeset);
|
|
1246
|
+
if (bound.nodeset !== null && !(Array.isArray(bound.nodeset) && bound.nodeset.length > 0)) {
|
|
1247
|
+
console.log('Node exists', bound.nodeset);
|
|
1034
1248
|
continue;
|
|
1035
1249
|
}
|
|
1036
|
-
|
|
1250
|
+
console.log('Node does not exists', bound.ref);
|
|
1037
1251
|
|
|
1038
1252
|
// We need to create that node!
|
|
1039
1253
|
const previousControl = boundControls[i - 1];
|
|
1040
1254
|
|
|
1041
1255
|
// Previous control can either be an ancestor of us, or a previous node, which can be a sibling, or a child of a sibling.
|
|
1042
1256
|
// First: parent
|
|
1043
|
-
if (previousControl.contains(bound)) {
|
|
1257
|
+
if (previousControl && previousControl.contains(bound)) {
|
|
1044
1258
|
// Parent is here.
|
|
1045
|
-
|
|
1046
|
-
|
|
1259
|
+
console.log('insert into', bound, previousControl);
|
|
1260
|
+
console.log('insert into nodeset', bound.nodeset);
|
|
1261
|
+
/**
|
|
1262
|
+
* @type {ParentNode}
|
|
1263
|
+
*/
|
|
1047
1264
|
const parentNodeset = previousControl.nodeset;
|
|
1048
1265
|
// console.log('parentNodeset', parentNodeset);
|
|
1049
1266
|
|
|
1050
1267
|
// const parentModelItemNode = parentModelItem.node;
|
|
1051
1268
|
const ref = bound.ref;
|
|
1052
1269
|
// const newElement = parentModelItemNode.ownerDocument.createElement(ref);
|
|
1053
|
-
if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
|
|
1054
|
-
|
|
1270
|
+
// if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
|
|
1271
|
+
// console.log(`Node with ref "${ref}" already exists.`);
|
|
1272
|
+
// continue;
|
|
1273
|
+
// }
|
|
1274
|
+
|
|
1275
|
+
const newNode = this._createNodes(ref, parentNodeset);
|
|
1276
|
+
if (!newNode) {
|
|
1277
|
+
// We could not make the node for some reason. Maybe it's something like `instance('XXX')`?
|
|
1055
1278
|
continue;
|
|
1056
1279
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1280
|
+
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
|
|
1281
|
+
parentNodeset.setAttributeNode(newNode);
|
|
1282
|
+
} else {
|
|
1283
|
+
const referenceNode = this._findReferenceNodeForNewElement(newNode, parentNodeset, null);
|
|
1284
|
+
if (referenceNode) {
|
|
1285
|
+
referenceNode.after(newNode);
|
|
1286
|
+
} else {
|
|
1287
|
+
parentNodeset.prepend(newNode);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1062
1290
|
bound.evalInContext();
|
|
1291
|
+
bound.getModelItem().bind?.evalInContext();
|
|
1292
|
+
|
|
1063
1293
|
// console.log('CREATED child', newElement);
|
|
1064
1294
|
// console.log('new control evaluated to ', control.nodeset);
|
|
1065
1295
|
// Done!
|
|
@@ -1068,7 +1298,7 @@ export class FxFore extends HTMLElement {
|
|
|
1068
1298
|
// console.log('previousControl', previousControl);
|
|
1069
1299
|
// console.log('control', control);
|
|
1070
1300
|
// Is previousControl a sibling or a descendant of a logical sibling? Keep looking backwards until we share parents!
|
|
1071
|
-
|
|
1301
|
+
let ourParent = XPathUtil.getParentBindingElement(bound);
|
|
1072
1302
|
// console.log('ourParent', ourParent);
|
|
1073
1303
|
let siblingControl = null;
|
|
1074
1304
|
/*
|
|
@@ -1088,28 +1318,56 @@ export class FxFore extends HTMLElement {
|
|
|
1088
1318
|
}
|
|
1089
1319
|
}
|
|
1090
1320
|
if (!siblingControl) {
|
|
1091
|
-
|
|
1321
|
+
console.log('No sibling found for', bound);
|
|
1092
1322
|
}
|
|
1093
1323
|
// console.log('sibling', siblingControl);
|
|
1094
1324
|
// todo: review: should this not just be inscopeContext?
|
|
1095
|
-
|
|
1325
|
+
let parentNodeset;
|
|
1326
|
+
if (!ourParent || !ourParent.nodeset) {
|
|
1327
|
+
/*
|
|
1328
|
+
if we lost context somehow just always assume default context and append to that
|
|
1329
|
+
instead of bailing out.
|
|
1330
|
+
*/
|
|
1331
|
+
parentNodeset = root.getModel().getDefaultContext();
|
|
1332
|
+
} else {
|
|
1333
|
+
parentNodeset = ourParent.nodeset;
|
|
1334
|
+
}
|
|
1096
1335
|
const ref = bound.ref;
|
|
1097
|
-
let referenceNodeset = siblingControl.nodeset;
|
|
1098
|
-
const newElement = this._createNodes(ref, parentNodeset);
|
|
1099
1336
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1337
|
+
const newNode = this._createNodes(ref, parentNodeset);
|
|
1338
|
+
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
|
|
1339
|
+
parentNodeset.setAttributeNode(newNode);
|
|
1340
|
+
} else {
|
|
1341
|
+
let referenceNode = this._findReferenceNodeForNewElement(
|
|
1342
|
+
newNode,
|
|
1343
|
+
parentNodeset,
|
|
1344
|
+
siblingControl,
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
if (referenceNode) {
|
|
1348
|
+
// console.log('insert after', referenceNode,newNode);
|
|
1349
|
+
if (referenceNode.nodeType === Node.DOCUMENT_NODE) {
|
|
1350
|
+
referenceNode.firstElementChild.append(newNode);
|
|
1351
|
+
} else {
|
|
1352
|
+
referenceNode.after(newNode);
|
|
1353
|
+
}
|
|
1354
|
+
} else {
|
|
1355
|
+
parentNodeset.prepend(newNode);
|
|
1356
|
+
}
|
|
1103
1357
|
}
|
|
1104
1358
|
|
|
1105
|
-
// Insert before the next sibling our our logical previous sibling
|
|
1106
|
-
parentNodeset.insertBefore(newElement, referenceNodeset.nextElementSibling);
|
|
1107
1359
|
/*
|
|
1108
1360
|
console.log('control inscope', control.getInScopeContext());
|
|
1109
1361
|
console.log('control ref', control.ref);
|
|
1110
1362
|
console.log('control new element parent', newElement.parentNode.nodeName);
|
|
1111
|
-
*/
|
|
1363
|
+
*/
|
|
1364
|
+
|
|
1112
1365
|
bound.evalInContext();
|
|
1366
|
+
bound.getModelItem().bind?.evalInContext();
|
|
1367
|
+
|
|
1368
|
+
if (!bound.nodeset) {
|
|
1369
|
+
throw new Error('Creating annode failed');
|
|
1370
|
+
}
|
|
1113
1371
|
// console.log('new control evaluated to ', control.nodeset);
|
|
1114
1372
|
// console.log('CREATED sibling', newElement);
|
|
1115
1373
|
}
|
|
@@ -1126,7 +1384,11 @@ export class FxFore extends HTMLElement {
|
|
|
1126
1384
|
return existingNode;
|
|
1127
1385
|
}
|
|
1128
1386
|
console.log(`creating new node for ref: ${ref}`);
|
|
1129
|
-
*/
|
|
1387
|
+
*/
|
|
1388
|
+
if (/instance\([^\)]*\)/.test(ref)) {
|
|
1389
|
+
// This is an absolute path for some instance. Not supporteed for now
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1130
1392
|
let newElement;
|
|
1131
1393
|
if (ref.includes('/')) {
|
|
1132
1394
|
// multi-step ref expressions
|
|
@@ -1261,21 +1523,6 @@ export class FxFore extends HTMLElement {
|
|
|
1261
1523
|
const repeats = this.querySelectorAll('[data-ref]');
|
|
1262
1524
|
if (repeats) {
|
|
1263
1525
|
Array.from(repeats).forEach(item => {
|
|
1264
|
-
if (item.closest('fx-control')) return;
|
|
1265
|
-
/*
|
|
1266
|
-
const parentRepeat = item.closest('fx-repeat');
|
|
1267
|
-
if(parentRepeat){
|
|
1268
|
-
this.dispatchEvent(
|
|
1269
|
-
new CustomEvent('log', {
|
|
1270
|
-
composed: false,
|
|
1271
|
-
bubbles: true,
|
|
1272
|
-
cancelable:true,
|
|
1273
|
-
detail: { id:this.id, message: `nesting elements with data-ref attributes within fx-repeat is not supported by now`, level:'Error'},
|
|
1274
|
-
}),
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
*/
|
|
1278
|
-
|
|
1279
1526
|
const table = item.parentNode.closest('table');
|
|
1280
1527
|
let host;
|
|
1281
1528
|
if (table) {
|