@jinntec/fore 2.5.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 +36088 -9
- package/dist/fore.js +35918 -9
- package/index.js +3 -1
- package/package.json +10 -4
- package/resources/fore.css +30 -5
- package/src/DataObserver.js +181 -0
- package/src/DependencyNotifyingDomFacade.js +27 -21
- package/src/DependentXPathQueries.js +32 -0
- package/src/ForeElementMixin.js +60 -26
- package/src/actions/abstract-action.js +24 -29
- package/src/actions/fx-append.js +25 -2
- package/src/actions/fx-call.js +2 -2
- package/src/actions/fx-delete.js +58 -21
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +62 -48
- package/src/actions/fx-load.js +7 -2
- 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 +20 -2
- package/src/actions/fx-show.js +4 -2
- 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 +78 -36
- package/src/fx-bind.js +128 -23
- package/src/fx-connection.js +24 -7
- package/src/fx-fore.js +552 -306
- package/src/fx-instance.js +9 -11
- package/src/fx-model.js +154 -65
- package/src/fx-submission.js +45 -51
- package/src/fx-var.js +5 -0
- package/src/getInScopeContext.js +8 -8
- package/src/modelitem.js +218 -72
- 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 +206 -0
- package/src/ui/abstract-control.js +15 -7
- package/src/ui/fx-case.js +15 -3
- package/src/ui/fx-container.js +10 -3
- package/src/ui/fx-control-menu.js +207 -0
- package/src/ui/fx-control.js +116 -32
- package/src/ui/fx-dialog.js +2 -2
- package/src/ui/fx-group.js +14 -0
- package/src/ui/fx-items.js +10 -4
- package/src/ui/fx-repeat-attributes.js +111 -35
- package/src/ui/fx-repeat.js +364 -87
- 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 +107 -11
- package/dist/fore-dev.js.map +0 -1
- package/dist/fore.js.map +0 -1
- package/src/ui/fx-select.js +0 -89
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;
|
|
@@ -247,14 +254,199 @@ export class FxFore extends HTMLElement {
|
|
|
247
254
|
// this.mergePartial = this.hasAttribute('merge-partial')? true:false;
|
|
248
255
|
this.mergePartial = false;
|
|
249
256
|
this.createNodes = this.hasAttribute('create-nodes') ? true : false;
|
|
257
|
+
this._localNamesWithChanges = new Set();
|
|
258
|
+
this.setAttribute('role', 'form'); // set aria role
|
|
259
|
+
}
|
|
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));
|
|
250
364
|
}
|
|
251
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
|
+
|
|
252
438
|
connectedCallback() {
|
|
439
|
+
const modelElement = Array.from(this.children).find(
|
|
440
|
+
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
this.model = modelElement;
|
|
444
|
+
|
|
253
445
|
this.style.visibility = 'hidden';
|
|
254
446
|
console.time('init');
|
|
255
447
|
this.strict = !!this.hasAttribute('strict');
|
|
256
448
|
/*
|
|
257
|
-
document.
|
|
449
|
+
document.re('ready', (e) =>{
|
|
258
450
|
if(e.target !== this){
|
|
259
451
|
// e.preventDefault();
|
|
260
452
|
console.log('>>> e', e);
|
|
@@ -293,57 +485,11 @@ export class FxFore extends HTMLElement {
|
|
|
293
485
|
|
|
294
486
|
this._injectDevtools();
|
|
295
487
|
|
|
296
|
-
const slot = this.shadowRoot.querySelector('slot#default');
|
|
297
|
-
slot.addEventListener('slotchange', async event => {
|
|
298
|
-
// preliminary addition for auto-conversion of non-prefixed element into prefixed elements. See fore.js
|
|
299
|
-
// console.log(`### <<<<< slotchange on '${this.id}' >>>>>`);
|
|
300
|
-
if (this.inited) return;
|
|
301
|
-
if (this.hasAttribute('convert')) {
|
|
302
|
-
this.replaceWith(Fore.copyDom(this));
|
|
303
|
-
// Fore.copyDom(this);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (this.ignoreExpressions) {
|
|
308
|
-
this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
|
|
309
|
-
}
|
|
488
|
+
// const slot = this.shadowRoot.querySelector('slot#default');
|
|
310
489
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
|
|
314
|
-
);
|
|
315
|
-
if (!modelElement) {
|
|
316
|
-
const generatedModel = document.createElement('fx-model');
|
|
317
|
-
this.appendChild(generatedModel);
|
|
318
|
-
modelElement = generatedModel;
|
|
319
|
-
// We are going to get a new slotchange event immediately, because we changed a slot.
|
|
320
|
-
// so cancel this one.
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (!modelElement.inited) {
|
|
324
|
-
console.info(
|
|
325
|
-
`%cFore is processing fx-fore#${this.id}`,
|
|
326
|
-
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
327
|
-
);
|
|
490
|
+
const slot = this.shadowRoot?.querySelector('slot') || this.querySelector('slot');
|
|
491
|
+
if (slot) slot.addEventListener('slotchange', this._onSlotChange);
|
|
328
492
|
|
|
329
|
-
const variables = new Map();
|
|
330
|
-
(function registerVariables(node) {
|
|
331
|
-
for (const child of node.children) {
|
|
332
|
-
if ('setInScopeVariables' in child) {
|
|
333
|
-
child.setInScopeVariables(variables);
|
|
334
|
-
}
|
|
335
|
-
registerVariables(child);
|
|
336
|
-
}
|
|
337
|
-
})(this);
|
|
338
|
-
|
|
339
|
-
await modelElement.modelConstruct();
|
|
340
|
-
this._handleModelConstructDone();
|
|
341
|
-
}
|
|
342
|
-
this.model = modelElement;
|
|
343
|
-
|
|
344
|
-
this._createRepeatsFromAttributes();
|
|
345
|
-
this.inited = true;
|
|
346
|
-
});
|
|
347
493
|
this.addEventListener('path-mutated', () => {
|
|
348
494
|
this.someInstanceDataStructureChanged = true;
|
|
349
495
|
});
|
|
@@ -376,16 +522,13 @@ export class FxFore extends HTMLElement {
|
|
|
376
522
|
}
|
|
377
523
|
|
|
378
524
|
/**
|
|
379
|
-
*
|
|
525
|
+
* Signal something happened with an element with the given local name. This will be used in the
|
|
526
|
+
* next (non-forceful) refresh to detect whether a component (usually a repeat) should update
|
|
380
527
|
*
|
|
381
|
-
* @param {
|
|
382
|
-
* @returns {void}
|
|
528
|
+
* @param {string} localNameOfElement
|
|
383
529
|
*/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (!found) {
|
|
387
|
-
this.toRefresh.push(modelItem);
|
|
388
|
-
}
|
|
530
|
+
signalChangeToElement(localNameOfElement) {
|
|
531
|
+
this._localNamesWithChanges.add(localNameOfElement);
|
|
389
532
|
}
|
|
390
533
|
|
|
391
534
|
/**
|
|
@@ -416,7 +559,14 @@ export class FxFore extends HTMLElement {
|
|
|
416
559
|
*/
|
|
417
560
|
async _loadFromSrc() {
|
|
418
561
|
// console.log('########## loading Fore from ', this.src, '##########');
|
|
419
|
-
|
|
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
|
+
}
|
|
420
570
|
}
|
|
421
571
|
|
|
422
572
|
/**
|
|
@@ -432,6 +582,8 @@ export class FxFore extends HTMLElement {
|
|
|
432
582
|
const { target } = entry;
|
|
433
583
|
|
|
434
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
|
|
435
587
|
if (fore.initialRun) return;
|
|
436
588
|
|
|
437
589
|
if (entry.isIntersecting) {
|
|
@@ -472,179 +624,151 @@ export class FxFore extends HTMLElement {
|
|
|
472
624
|
*/
|
|
473
625
|
}
|
|
474
626
|
|
|
475
|
-
/**
|
|
476
|
-
* refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
|
|
477
|
-
* the bound modelItem to the bound element.
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* force - boolean - if true will refresh all children disregarding toRefresh array
|
|
481
|
-
*
|
|
482
|
-
*/
|
|
483
|
-
async forceRefresh() {
|
|
484
|
-
// console.time('refresh');
|
|
485
|
-
// console.group('### forced refresh', this);
|
|
486
|
-
|
|
487
|
-
Fore.refreshChildren(this, true);
|
|
488
|
-
this._updateTemplateExpressions();
|
|
489
|
-
this._scanForNewTemplateExpressionsNextRefresh = false; // reset
|
|
490
|
-
this._processTemplateExpressions();
|
|
491
|
-
|
|
492
|
-
// console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
|
|
493
|
-
|
|
494
|
-
Fore.dispatch(this, 'refresh-done', {});
|
|
495
|
-
|
|
496
|
-
// console.groupEnd();
|
|
497
|
-
// console.timeEnd('refresh');
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// async refresh(force, changedPaths) {
|
|
501
627
|
/**
|
|
502
628
|
* @param {(boolean|{reason:'index-function'})} [force]fx-fore
|
|
503
629
|
*/
|
|
504
630
|
async refresh(force) {
|
|
505
|
-
/*
|
|
506
|
-
|
|
507
|
-
if (!changedPaths) {
|
|
508
|
-
changedPaths = this.toRefresh.map(item => item.path);
|
|
509
|
-
} else {
|
|
510
|
-
this.toRefresh.push(
|
|
511
|
-
...changedPaths
|
|
512
|
-
.map(
|
|
513
|
-
path =>
|
|
514
|
-
this.getModel()
|
|
515
|
-
.modelItems
|
|
516
|
-
.find(item => item.path === path)
|
|
517
|
-
)
|
|
518
|
-
.filter(Boolean)
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
for(const changedPath of changedPaths) {
|
|
522
|
-
for (const repeat of this.querySelectorAll('fx-repeat')) {
|
|
523
|
-
if (repeat.closest('fx-fore') !== this) {
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (repeat.touchedPaths && repeat.touchedPaths.has(changedPath)) {
|
|
528
|
-
// Make a temporary model-item-like structure for this
|
|
529
|
-
this.toRefresh.push({
|
|
530
|
-
path: changedPath,
|
|
531
|
-
boundControls: [repeat]
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
console.log('Found a repeat to update!!!', repeat)
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
*/
|
|
540
631
|
if (this.isRefreshing) {
|
|
541
632
|
return;
|
|
542
633
|
}
|
|
634
|
+
|
|
635
|
+
/*
|
|
636
|
+
if (force !== true && this._localNamesWithChanges.size > 0) {
|
|
637
|
+
force = {
|
|
638
|
+
...(force || { reason: undefined }),
|
|
639
|
+
elementLocalnamesWithChanges: Array.from(this._localNamesWithChanges),
|
|
640
|
+
};
|
|
641
|
+
this._localNamesWithChanges.clear();
|
|
642
|
+
}
|
|
643
|
+
*/
|
|
644
|
+
|
|
543
645
|
this.isRefreshing = true;
|
|
544
|
-
|
|
646
|
+
this.isRefreshPhase = true;
|
|
545
647
|
|
|
546
648
|
// refresh () {
|
|
547
649
|
// ### refresh Fore UI elements
|
|
548
650
|
// if (!this.initialRun && this.toRefresh.length !== 0) {
|
|
549
|
-
if (!
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
this.toRefresh.forEach(modelItem => {
|
|
555
|
-
// check if modelItem has boundControls - if so, call refresh() for each of them
|
|
556
|
-
const controlsToRefresh = modelItem.boundControls;
|
|
557
|
-
if (controlsToRefresh) {
|
|
558
|
-
controlsToRefresh.forEach(ctrl => {
|
|
559
|
-
ctrl.refresh(force);
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ### check if other controls depend on current modelItem
|
|
564
|
-
const { mainGraph } = this.getModel();
|
|
565
|
-
if (mainGraph && mainGraph.hasNode(modelItem.path)) {
|
|
566
|
-
const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
|
|
567
|
-
// ### iterate dependant modelItems and refresh all their boundControls
|
|
568
|
-
if (deps.length !== 0) {
|
|
569
|
-
deps.forEach(dep => {
|
|
570
|
-
// ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
|
|
571
|
-
const basePath = XPathUtil.getBasePath(dep);
|
|
572
|
-
const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
|
|
573
|
-
// ### refresh all boundControls
|
|
574
|
-
modelItemOfDep.boundControls.forEach(control => {
|
|
575
|
-
control.refresh(force);
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
needsRefresh = true;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
this.toRefresh = [];
|
|
583
|
-
/*
|
|
584
|
-
if (!needsRefresh) {
|
|
585
|
-
console.log('no dependants to refresh');
|
|
586
|
-
}
|
|
587
|
-
*/
|
|
651
|
+
// if (!this.initialRun && this.toRefresh.length !== 0) {
|
|
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);
|
|
588
656
|
} else {
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
Array.from(visited).forEach(v =>{
|
|
593
|
-
v.classList.remove('visited');
|
|
594
|
-
});
|
|
595
|
-
*/
|
|
596
|
-
|
|
597
|
-
if (this.inited) {
|
|
598
|
-
Fore.refreshChildren(this, force);
|
|
599
|
-
}
|
|
600
|
-
// 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();
|
|
601
660
|
}
|
|
602
661
|
|
|
603
662
|
// ### refresh template expressions
|
|
604
|
-
if (this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
|
|
663
|
+
if (force === true || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
|
|
605
664
|
this._updateTemplateExpressions();
|
|
606
665
|
this._scanForNewTemplateExpressionsNextRefresh = false; // reset
|
|
607
666
|
}
|
|
608
667
|
|
|
609
668
|
this._processTemplateExpressions();
|
|
610
669
|
|
|
670
|
+
this.isRefreshPhase = false;
|
|
671
|
+
|
|
611
672
|
// console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
|
|
612
673
|
// this.dispatchEvent(new CustomEvent('refresh-done'));
|
|
613
|
-
|
|
674
|
+
this.initialRun = false;
|
|
614
675
|
this.style.visibility = 'visible';
|
|
615
676
|
console.info(
|
|
616
|
-
`%
|
|
677
|
+
`%c ✅ refresh-done on #${this.id}`,
|
|
617
678
|
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
679
|
+
this.getModel().modelItems,
|
|
618
680
|
);
|
|
619
681
|
|
|
620
682
|
Fore.dispatch(this, 'refresh-done', {});
|
|
621
683
|
|
|
622
|
-
// this.isRefreshing = true;
|
|
623
|
-
// this.parentNode.closest('fx-fore')?.refresh(false, changedPaths);
|
|
624
|
-
|
|
625
684
|
const subFores = Array.from(this.querySelectorAll('fx-fore'));
|
|
626
685
|
/*
|
|
627
|
-
|
|
628
|
-
|
|
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.
|
|
629
688
|
|
|
630
|
-
|
|
689
|
+
This only affects the refreshing NOT the data mutation itself which is happening as expected.
|
|
631
690
|
|
|
632
|
-
|
|
633
|
-
|
|
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.
|
|
634
693
|
|
|
635
|
-
|
|
694
|
+
So refreshed propagate downwards but not upwards which is at least an option to consider.
|
|
636
695
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
696
|
+
if(this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE){
|
|
697
|
+
// await this.parentNode.closest('fx-fore')?.refresh(false);
|
|
698
|
+
}
|
|
640
699
|
*/
|
|
641
700
|
for (const subFore of subFores) {
|
|
642
701
|
// subFore.refresh(false, changedPaths);
|
|
643
702
|
if (subFore.ready) {
|
|
644
|
-
|
|
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?
|
|
705
|
+
await subFore.refresh(true);
|
|
645
706
|
}
|
|
646
707
|
}
|
|
647
708
|
this.isRefreshing = false;
|
|
709
|
+
// Clear the batch
|
|
710
|
+
// this.batchedNotifications.clear();
|
|
711
|
+
}
|
|
712
|
+
|
|
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
|
+
}
|
|
723
|
+
|
|
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
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
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
|
+
}
|
|
648
772
|
}
|
|
649
773
|
|
|
650
774
|
/**
|
|
@@ -657,7 +781,7 @@ export class FxFore extends HTMLElement {
|
|
|
657
781
|
*/
|
|
658
782
|
_updateTemplateExpressions() {
|
|
659
783
|
const search =
|
|
660
|
-
"(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])]";
|
|
661
785
|
|
|
662
786
|
const tmplExpressions = evaluateXPathToNodes(search, this, this);
|
|
663
787
|
// console.log('template expressions found ', tmplExpressions);
|
|
@@ -669,8 +793,8 @@ export class FxFore extends HTMLElement {
|
|
|
669
793
|
// console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
|
|
670
794
|
|
|
671
795
|
/*
|
|
672
|
-
|
|
673
|
-
|
|
796
|
+
storing expressions and their nodes for re-evaluation
|
|
797
|
+
*/
|
|
674
798
|
Array.from(tmplExpressions).forEach(node => {
|
|
675
799
|
const ele = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentNode;
|
|
676
800
|
if (ele.closest('fx-fore') !== this) {
|
|
@@ -695,6 +819,7 @@ export class FxFore extends HTMLElement {
|
|
|
695
819
|
}
|
|
696
820
|
|
|
697
821
|
_processTemplateExpressions() {
|
|
822
|
+
console.log('processing template expressions ', this.storedTemplateExpressionByNode);
|
|
698
823
|
for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
|
|
699
824
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
700
825
|
// Attribute nodes are not contained by the document, but their owner elements are!
|
|
@@ -758,10 +883,11 @@ export class FxFore extends HTMLElement {
|
|
|
758
883
|
: this.getModel().getDefaultInstance();
|
|
759
884
|
|
|
760
885
|
try {
|
|
761
|
-
|
|
886
|
+
const result = evaluateXPathToString(naked, inscope, node, null, inst);
|
|
887
|
+
// console.log(`template expression result for ${naked}=${result}`);
|
|
888
|
+
return result;
|
|
762
889
|
} catch (error) {
|
|
763
890
|
console.warn('ignoring unparseable expr', error);
|
|
764
|
-
|
|
765
891
|
return match;
|
|
766
892
|
}
|
|
767
893
|
});
|
|
@@ -908,45 +1034,6 @@ export class FxFore extends HTMLElement {
|
|
|
908
1034
|
return parent;
|
|
909
1035
|
}
|
|
910
1036
|
|
|
911
|
-
/*
|
|
912
|
-
_createStep(){
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
*/
|
|
916
|
-
|
|
917
|
-
/*
|
|
918
|
-
_generateInstance(start, parent) {
|
|
919
|
-
if (start.hasAttribute('ref')) {
|
|
920
|
-
const ref = start.getAttribute('ref');
|
|
921
|
-
|
|
922
|
-
if(ref.includes('/')){
|
|
923
|
-
console.log('complex path to create ', ref);
|
|
924
|
-
const steps = ref.split('/');
|
|
925
|
-
steps.forEach(step => {
|
|
926
|
-
console.log('step ', step);
|
|
927
|
-
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// const generated = document.createElement(ref);
|
|
932
|
-
const generated = parent.ownerDocument.createElement(ref);
|
|
933
|
-
if (start.children.length === 0) {
|
|
934
|
-
generated.textContent = start.textContent;
|
|
935
|
-
}
|
|
936
|
-
parent.appendChild(generated);
|
|
937
|
-
parent = generated;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
if (start.hasChildNodes()) {
|
|
941
|
-
const list = start.children;
|
|
942
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
943
|
-
this._generateInstance(list[i], parent);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
return parent;
|
|
947
|
-
}
|
|
948
|
-
*/
|
|
949
|
-
|
|
950
1037
|
/**
|
|
951
1038
|
* Start the initialization of the UI by
|
|
952
1039
|
*
|
|
@@ -958,29 +1045,39 @@ export class FxFore extends HTMLElement {
|
|
|
958
1045
|
* @private
|
|
959
1046
|
*/
|
|
960
1047
|
async _initUI() {
|
|
961
|
-
// console.log('### _initUI()');
|
|
962
1048
|
console.info(
|
|
963
1049
|
`%cinitUI #${this.id}`,
|
|
964
1050
|
'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
965
1051
|
);
|
|
966
1052
|
|
|
967
|
-
|
|
1053
|
+
const parentFore = this.closest('fx-fore');
|
|
1054
|
+
if (parentFore) {
|
|
1055
|
+
this.initialRun = false;
|
|
1056
|
+
} else {
|
|
1057
|
+
if (!this.initialRun) return;
|
|
1058
|
+
}
|
|
968
1059
|
this.classList.add('initialRun');
|
|
969
1060
|
await this._lazyCreateInstance();
|
|
970
1061
|
|
|
971
1062
|
/*
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1063
|
+
const options = {
|
|
1064
|
+
root: null,
|
|
1065
|
+
rootMargin: '0px',
|
|
1066
|
+
threshold: 0.3,
|
|
1067
|
+
};
|
|
1068
|
+
*/
|
|
978
1069
|
|
|
979
1070
|
// First refresh should be forced
|
|
980
1071
|
if (this.createNodes) {
|
|
981
1072
|
this.initData();
|
|
1073
|
+
const binds = this.getModel().querySelector('fx-bind');
|
|
1074
|
+
if (binds) {
|
|
1075
|
+
this.getModel().updateModel();
|
|
1076
|
+
}
|
|
982
1077
|
}
|
|
1078
|
+
// await this.forceRefresh();
|
|
983
1079
|
await this.refresh(true);
|
|
1080
|
+
// await Fore.initUI(this);
|
|
984
1081
|
|
|
985
1082
|
// this.style.display='block'
|
|
986
1083
|
this.classList.add('fx-ready');
|
|
@@ -990,8 +1087,8 @@ export class FxFore extends HTMLElement {
|
|
|
990
1087
|
this.initialRun = false;
|
|
991
1088
|
// console.log('### >>>>> dispatching ready >>>>>', this);
|
|
992
1089
|
console.info(
|
|
993
|
-
`%c
|
|
994
|
-
'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%;',
|
|
995
1092
|
);
|
|
996
1093
|
|
|
997
1094
|
// console.log(`### <<<<< ${this.id} ready >>>>>`);
|
|
@@ -999,7 +1096,6 @@ export class FxFore extends HTMLElement {
|
|
|
999
1096
|
// console.log('### modelItems: ', this.getModel().modelItems);
|
|
1000
1097
|
Fore.dispatch(this, 'ready', {});
|
|
1001
1098
|
// console.log('dataChanged', FxModel.dataChanged);
|
|
1002
|
-
console.timeEnd('init');
|
|
1003
1099
|
|
|
1004
1100
|
this.addEventListener('dragstart', this._handleDragStart);
|
|
1005
1101
|
// this.addEventListener('dragend', this._handleDragEnd);
|
|
@@ -1012,6 +1108,110 @@ export class FxFore extends HTMLElement {
|
|
|
1012
1108
|
});
|
|
1013
1109
|
}
|
|
1014
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
|
+
|
|
1015
1215
|
/**
|
|
1016
1216
|
* @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
|
|
1017
1217
|
*
|
|
@@ -1019,50 +1219,78 @@ export class FxFore extends HTMLElement {
|
|
|
1019
1219
|
initData(root = this) {
|
|
1020
1220
|
// const created = new Promise(resolve => {
|
|
1021
1221
|
console.log('INIT');
|
|
1022
|
-
const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
|
|
1023
|
-
|
|
1222
|
+
// const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* @type {import('./ForeElementMixin.js').default[]}
|
|
1226
|
+
*/
|
|
1227
|
+
const boundControls = Array.from(
|
|
1228
|
+
root.querySelectorAll(
|
|
1229
|
+
'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
|
|
1230
|
+
),
|
|
1231
|
+
);
|
|
1232
|
+
if (root.matches && root.matches('fx-repeatitem')) {
|
|
1024
1233
|
boundControls.unshift(root);
|
|
1025
1234
|
}
|
|
1026
|
-
|
|
1235
|
+
console.log('_initData', boundControls);
|
|
1027
1236
|
for (let i = 0; i < boundControls.length; i++) {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1237
|
+
const bound = boundControls[i];
|
|
1238
|
+
|
|
1239
|
+
/*
|
|
1240
|
+
ignore bound elements that are enclosed with a control like <select> or <fx-items> and repeated items
|
|
1241
|
+
*/
|
|
1242
|
+
if (!bound.matches('fx-repeatitem') && !bound.parentNode.closest('fx-control')) {
|
|
1030
1243
|
// Repeat items are dumb. They do not respond to evalInContext
|
|
1031
|
-
|
|
1244
|
+
bound.evalInContext();
|
|
1032
1245
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
// 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);
|
|
1036
1248
|
continue;
|
|
1037
1249
|
}
|
|
1038
|
-
|
|
1250
|
+
console.log('Node does not exists', bound.ref);
|
|
1039
1251
|
|
|
1040
1252
|
// We need to create that node!
|
|
1041
1253
|
const previousControl = boundControls[i - 1];
|
|
1042
1254
|
|
|
1043
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.
|
|
1044
1256
|
// First: parent
|
|
1045
|
-
if (previousControl.contains(
|
|
1257
|
+
if (previousControl && previousControl.contains(bound)) {
|
|
1046
1258
|
// Parent is here.
|
|
1047
|
-
|
|
1048
|
-
|
|
1259
|
+
console.log('insert into', bound, previousControl);
|
|
1260
|
+
console.log('insert into nodeset', bound.nodeset);
|
|
1261
|
+
/**
|
|
1262
|
+
* @type {ParentNode}
|
|
1263
|
+
*/
|
|
1049
1264
|
const parentNodeset = previousControl.nodeset;
|
|
1050
1265
|
// console.log('parentNodeset', parentNodeset);
|
|
1051
1266
|
|
|
1052
1267
|
// const parentModelItemNode = parentModelItem.node;
|
|
1053
|
-
const ref =
|
|
1268
|
+
const ref = bound.ref;
|
|
1054
1269
|
// const newElement = parentModelItemNode.ownerDocument.createElement(ref);
|
|
1055
|
-
if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
|
|
1056
|
-
|
|
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')`?
|
|
1057
1278
|
continue;
|
|
1058
1279
|
}
|
|
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
|
+
}
|
|
1290
|
+
bound.evalInContext();
|
|
1291
|
+
bound.getModelItem().bind?.evalInContext();
|
|
1059
1292
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
// Plonk it in at the start!
|
|
1063
|
-
parentNodeset.insertBefore(newElement, parentNodeset.firstChild);
|
|
1064
|
-
control.evalInContext();
|
|
1065
|
-
console.log('CREATED child', newElement);
|
|
1293
|
+
// console.log('CREATED child', newElement);
|
|
1066
1294
|
// console.log('new control evaluated to ', control.nodeset);
|
|
1067
1295
|
// Done!
|
|
1068
1296
|
continue;
|
|
@@ -1070,7 +1298,7 @@ export class FxFore extends HTMLElement {
|
|
|
1070
1298
|
// console.log('previousControl', previousControl);
|
|
1071
1299
|
// console.log('control', control);
|
|
1072
1300
|
// Is previousControl a sibling or a descendant of a logical sibling? Keep looking backwards until we share parents!
|
|
1073
|
-
|
|
1301
|
+
let ourParent = XPathUtil.getParentBindingElement(bound);
|
|
1074
1302
|
// console.log('ourParent', ourParent);
|
|
1075
1303
|
let siblingControl = null;
|
|
1076
1304
|
/*
|
|
@@ -1090,29 +1318,58 @@ export class FxFore extends HTMLElement {
|
|
|
1090
1318
|
}
|
|
1091
1319
|
}
|
|
1092
1320
|
if (!siblingControl) {
|
|
1093
|
-
|
|
1321
|
+
console.log('No sibling found for', bound);
|
|
1094
1322
|
}
|
|
1095
1323
|
// console.log('sibling', siblingControl);
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1324
|
+
// todo: review: should this not just be inscopeContext?
|
|
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
|
+
}
|
|
1335
|
+
const ref = bound.ref;
|
|
1336
|
+
|
|
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
|
+
}
|
|
1104
1357
|
}
|
|
1105
1358
|
|
|
1106
|
-
// Insert before the next sibling our our logical previous sibling
|
|
1107
|
-
parentNodeset.insertBefore(newElement, referenceNodeset.nextElementSibling);
|
|
1108
1359
|
/*
|
|
1109
1360
|
console.log('control inscope', control.getInScopeContext());
|
|
1110
1361
|
console.log('control ref', control.ref);
|
|
1111
1362
|
console.log('control new element parent', newElement.parentNode.nodeName);
|
|
1112
|
-
*/
|
|
1113
|
-
|
|
1363
|
+
*/
|
|
1364
|
+
|
|
1365
|
+
bound.evalInContext();
|
|
1366
|
+
bound.getModelItem().bind?.evalInContext();
|
|
1367
|
+
|
|
1368
|
+
if (!bound.nodeset) {
|
|
1369
|
+
throw new Error('Creating annode failed');
|
|
1370
|
+
}
|
|
1114
1371
|
// console.log('new control evaluated to ', control.nodeset);
|
|
1115
|
-
console.log('CREATED sibling', newElement);
|
|
1372
|
+
// console.log('CREATED sibling', newElement);
|
|
1116
1373
|
}
|
|
1117
1374
|
// console.log('DATA', this.getModel().getDefaultContext());
|
|
1118
1375
|
}
|
|
@@ -1126,16 +1383,20 @@ export class FxFore extends HTMLElement {
|
|
|
1126
1383
|
console.log(`Node already exists for ref: ${ref}`);
|
|
1127
1384
|
return existingNode;
|
|
1128
1385
|
}
|
|
1129
|
-
*/
|
|
1130
1386
|
console.log(`creating new node for ref: ${ref}`);
|
|
1387
|
+
*/
|
|
1388
|
+
if (/instance\([^\)]*\)/.test(ref)) {
|
|
1389
|
+
// This is an absolute path for some instance. Not supporteed for now
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1131
1392
|
let newElement;
|
|
1132
1393
|
if (ref.includes('/')) {
|
|
1133
1394
|
// multi-step ref expressions
|
|
1134
|
-
newElement = XPathUtil.
|
|
1395
|
+
newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
|
|
1135
1396
|
// console.log('new subtree', newElement);
|
|
1136
1397
|
return newElement;
|
|
1137
1398
|
} else {
|
|
1138
|
-
return XPathUtil.
|
|
1399
|
+
return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
|
|
1139
1400
|
}
|
|
1140
1401
|
}
|
|
1141
1402
|
|
|
@@ -1262,21 +1523,6 @@ export class FxFore extends HTMLElement {
|
|
|
1262
1523
|
const repeats = this.querySelectorAll('[data-ref]');
|
|
1263
1524
|
if (repeats) {
|
|
1264
1525
|
Array.from(repeats).forEach(item => {
|
|
1265
|
-
if (item.closest('fx-control')) return;
|
|
1266
|
-
/*
|
|
1267
|
-
const parentRepeat = item.closest('fx-repeat');
|
|
1268
|
-
if(parentRepeat){
|
|
1269
|
-
this.dispatchEvent(
|
|
1270
|
-
new CustomEvent('log', {
|
|
1271
|
-
composed: false,
|
|
1272
|
-
bubbles: true,
|
|
1273
|
-
cancelable:true,
|
|
1274
|
-
detail: { id:this.id, message: `nesting elements with data-ref attributes within fx-repeat is not supported by now`, level:'Error'},
|
|
1275
|
-
}),
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1278
|
-
*/
|
|
1279
|
-
|
|
1280
1526
|
const table = item.parentNode.closest('table');
|
|
1281
1527
|
let host;
|
|
1282
1528
|
if (table) {
|