@jinntec/fore 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/fore-dev.js +4472 -2645
  2. package/dist/fore.js +4417 -2622
  3. package/index.js +2 -0
  4. package/package.json +10 -4
  5. package/resources/fore.css +2 -6
  6. package/src/DependencyNotifyingDomFacade.js +27 -21
  7. package/src/ForeElementMixin.js +282 -277
  8. package/src/actions/abstract-action.js +7 -12
  9. package/src/actions/fx-append.js +23 -2
  10. package/src/actions/fx-call.js +2 -2
  11. package/src/actions/fx-delete.js +54 -29
  12. package/src/actions/fx-insert.js +23 -15
  13. package/src/actions/fx-load.js +2 -2
  14. package/src/actions/fx-refresh.js +15 -5
  15. package/src/actions/fx-replace.js +18 -3
  16. package/src/actions/fx-reset.js +6 -0
  17. package/src/actions/fx-send.js +10 -7
  18. package/src/actions/fx-setattribute.js +12 -0
  19. package/src/actions/fx-setfocus.js +11 -8
  20. package/src/actions/fx-setvalue.js +93 -93
  21. package/src/actions/fx-toggle.js +1 -1
  22. package/src/extract-predicate-deps.js +57 -0
  23. package/src/extractPredicateDependencies.js +36 -0
  24. package/src/fore.js +41 -16
  25. package/src/fx-bind.js +128 -23
  26. package/src/fx-connection.js +24 -7
  27. package/src/fx-fore.js +506 -259
  28. package/src/fx-instance.js +9 -11
  29. package/src/fx-model.js +154 -65
  30. package/src/fx-submission.js +19 -30
  31. package/src/fx-var.js +5 -0
  32. package/src/getInScopeContext.js +7 -8
  33. package/src/modelitem.js +247 -112
  34. package/src/tools/fx-action-log.js +21 -19
  35. package/src/tools/fx-log-settings.js +4 -2
  36. package/src/ui/TemplateExpression.js +12 -0
  37. package/src/ui/UIElement.js +125 -10
  38. package/src/ui/abstract-control.js +5 -0
  39. package/src/ui/fx-case.js +15 -3
  40. package/src/ui/fx-container.js +5 -0
  41. package/src/ui/fx-control-menu.js +23 -14
  42. package/src/ui/fx-control.js +55 -15
  43. package/src/ui/fx-items.js +10 -4
  44. package/src/ui/fx-repeat-attributes.js +111 -35
  45. package/src/ui/fx-repeat.js +332 -85
  46. package/src/ui/fx-repeatitem.js +23 -20
  47. package/src/ui/fx-switch.js +5 -3
  48. package/src/ui/fx-upload.js +36 -40
  49. package/src/ui/repeat-base.js +532 -0
  50. package/src/withDraggability.js +8 -4
  51. package/src/xpath-evaluation.js +26 -8
  52. package/src/xpath-path.js +79 -0
  53. package/src/xpath-util.js +357 -289
@@ -7,6 +7,9 @@ import getInScopeContext from '../getInScopeContext.js';
7
7
  import { XPathUtil } from '../xpath-util.js';
8
8
  import { withDraggability } from '../withDraggability.js';
9
9
  import { UIElement } from './UIElement.js';
10
+ import { getPath } from '../xpath-path.js';
11
+ import { FxModel } from '../fx-model.js';
12
+ import { FxBind } from '../fx-bind.js';
10
13
 
11
14
  // import {DependencyNotifyingDomFacade} from '../DependencyNotifyingDomFacade';
12
15
 
@@ -65,46 +68,13 @@ export class FxRepeat extends withDraggability(UIElement, false) {
65
68
  this.index = 1;
66
69
  this.repeatSize = 0;
67
70
  this.attachShadow({ mode: 'open', delegatesFocus: true });
68
- }
69
-
70
- get repeatSize() {
71
- return this.querySelectorAll(':scope > fx-repeatitem').length;
72
- }
73
-
74
- set repeatSize(size) {
75
- this.size = size;
76
- }
77
-
78
- setIndex(index) {
79
- // console.log('new repeat index ', index);
80
- this.index = index;
81
- const rItems = this.querySelectorAll(':scope > fx-repeatitem');
82
- this.applyIndex(rItems[this.index - 1]);
83
-
84
- this.getOwnerForm().refresh({ reason: 'index-function', elementLocalnamesWithChanges: [] });
85
- }
86
-
87
- applyIndex(repeatItem) {
88
- this._removeIndexMarker();
89
- if (repeatItem) {
90
- repeatItem.setAttribute('repeat-index', '');
91
- }
92
- }
93
-
94
- get index() {
95
- return parseInt(this.getAttribute('index'), 10);
96
- }
97
-
98
- set index(idx) {
99
- this.setAttribute('index', idx);
100
- }
101
-
102
- _getRef() {
103
- return this.getAttribute('ref');
71
+ this.opNum = 0; // global number of operations
104
72
  }
105
73
 
106
74
  connectedCallback() {
107
75
  super.connectedCallback();
76
+ this.template = this.querySelector('template');
77
+
108
78
  // console.log('connectedCallback',this);
109
79
  // this.display = window.getComputedStyle(this, null).getPropertyValue("display");
110
80
  this.ref = this.getAttribute('ref');
@@ -113,49 +83,134 @@ export class FxRepeat extends withDraggability(UIElement, false) {
113
83
  // console.log('### fx-repeat connected ', this.id);
114
84
  this.addEventListener('item-changed', e => {
115
85
  const { item } = e.detail;
116
- const idx = Array.from(this.children).indexOf(item);
117
- // Warning: index is one-based
118
- this.setIndex(idx + 1);
86
+ this.setIndex(item.index);
119
87
  });
120
- // todo: review - this is just used by append action - event consolidation ?
121
- document.addEventListener('index-changed', e => {
122
- e.stopPropagation();
123
- if (!e.target === this) return;
124
- // const { item } = e.detail;
125
- // const idx = Array.from(this.children).indexOf(item);
126
- const { index } = e.detail;
127
- this.index = parseInt(index, 10);
128
- this.applyIndex(this.children[index - 1]);
129
- });
130
- /*
131
- document.addEventListener('insert', e => {
132
- const nodes = e.detail.insertedNodes;
133
- this.index = e.detail.position;
134
- console.log('insert catched', nodes, this.index);
135
- });
136
- */
88
+
89
+ // Listen for insertion events
90
+ this.handleInsertHandler = event => {
91
+ const { detail } = event;
92
+ const myForeId = this.getOwnerForm().id;
93
+ if (myForeId !== detail.foreId) {
94
+ return;
95
+ }
96
+ // todo: early out if this.ref does not match the ref of the inserted node. Avoid re-evaluating the nodeset
97
+ // if (this.ref !== detail.ref) return;
98
+
99
+ console.log('insert catched', detail);
100
+
101
+ // Step 1: Refresh/re-evaluate the nodeset
102
+ const oldNodesetLength = this.nodeset.length;
103
+ this._evalNodeset();
104
+ const newNodesetLength = this.nodeset.length;
105
+ if (oldNodesetLength === newNodesetLength) {
106
+ return;
107
+ }
108
+
109
+ /**
110
+ * @type {number}
111
+ */
112
+ // const insertionIndex = detail.index;
113
+ /**
114
+ * The newly inserted node. TODO: handle multiple?
115
+ * @type {Node}
116
+ */
117
+ const insertedNode = detail.insertedNodes;
118
+ const insertionIndex = this.nodeset.indexOf(insertedNode) + 1;
119
+ // Step 2: Get current repeat items and create a new item
120
+ /**
121
+ * @type {import('./fx-repeatitem.js').FxRepeatitem[]}
122
+ */
123
+ const repeatItems = Array.from(
124
+ this.querySelectorAll(
125
+ ':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
126
+ ),
127
+ );
128
+
129
+ // todo: search fx-bind elements with same nodeset as this repeat - if present update modelItem instead of creating one
130
+ const newRepeatItem = this._createNewRepeatItem();
131
+
132
+ // Step 3: Insert the new repeatItem at the correct position
133
+ const beforeNode = repeatItems[insertionIndex - 1] ?? null; // Null appends by default
134
+ this.insertBefore(newRepeatItem, beforeNode);
135
+ newRepeatItem.index = insertionIndex;
136
+ this._initVariables(newRepeatItem);
137
+
138
+ // Step 4: Assign the inserted nodeset to the new `repeatItem`
139
+ newRepeatItem.nodeset = detail.insertedNodes;
140
+
141
+ // Update all the indices following here
142
+ for (let i = insertionIndex - 1; i < repeatItems.length; ++i) {
143
+ const sibling = repeatItems[i];
144
+ // TODO: handle the next ones
145
+ sibling.index += 1;
146
+ }
147
+
148
+ this.setIndex(insertionIndex); // sets attribute + applies repeat-index + refresh
149
+
150
+ // Generate the parent `modelItem` for the new repeat item
151
+ this.opNum++;
152
+ const parentModelItem = FxBind.createModelItem(
153
+ this.ref,
154
+ detail.insertedNodes,
155
+ newRepeatItem,
156
+ this.opNum,
157
+ );
158
+ newRepeatItem.modelItem = parentModelItem;
159
+
160
+ this.getModel().registerModelItem(parentModelItem);
161
+
162
+ // Step 5: Create modelItems recursively for child elements
163
+ this._createModelItemsRecursively(newRepeatItem, parentModelItem);
164
+ // Step 6: Notify and refresh the UI
165
+ this.getOwnerForm().scanForNewTemplateExpressionsNextRefresh();
166
+ this.getOwnerForm().addToBatchedNotifications(newRepeatItem);
167
+ };
168
+ this.handleDeleteHandler = event => {
169
+ console.log('delete catched', event);
170
+ const { detail } = event;
171
+ if (!detail || !detail.deletedNodes) {
172
+ return;
173
+ }
174
+
175
+ // Remove corresponding repeat items for deleted nodes
176
+ detail.deletedNodes.forEach(node => {
177
+ this.handleDelete(node);
178
+ // this.removeRepeatItemForNode(node);
179
+ });
180
+ this.getOwnerForm().addToBatchedNotifications(this);
181
+ };
182
+ // inside connectedCallback()
183
+ document.addEventListener('insert', this.handleInsertHandler, true);
184
+ document.addEventListener('deleted', this.handleDeleteHandler, true);
137
185
 
138
186
  // if (this.getOwnerForm().lazyRefresh) {
187
+ /**
188
+ * @type {MutationRecord[]}
189
+ */
190
+ let bufferedMutationRecords = [];
191
+ let debouncedOnMutations = null;
139
192
  this.mutationObserver = new MutationObserver(mutations => {
140
- // console.log('mutations', mutations);
141
-
142
- if (mutations[0].type === 'childList') {
143
- const added = mutations[0].addedNodes[0];
144
- if (added) {
145
- const instance = XPathUtil.resolveInstance(this, this.ref);
146
- const path = XPathUtil.getPath(added, instance);
147
- // console.log('path mutated', path);
148
- // this.dispatch('path-mutated',{'path':path,'nodeset':this.nodeset,'index': this.index});
149
- // this.index = index;
150
- // const prev = mutations[0].previousSibling.previousElementSibling;
151
- // const index = prev.index();
152
- // this.applyIndex(this.index -1);
153
-
154
- Fore.dispatch(this, 'path-mutated', { path, index: this.index });
155
- }
193
+ bufferedMutationRecords.push(...mutations);
194
+ if (!debouncedOnMutations) {
195
+ debouncedOnMutations = new Promise(() => {
196
+ debouncedOnMutations = null;
197
+ const records = bufferedMutationRecords;
198
+ bufferedMutationRecords = [];
199
+ const shouldRefresh = false;
200
+ for (const mutation of records) {
201
+ if (mutation.type === 'childList') {
202
+ const added = mutation.addedNodes[0];
203
+ if (added) {
204
+ const instance = XPathUtil.resolveInstance(this, this.ref);
205
+ const path = getPath(added, instance);
206
+ Fore.dispatch(this, 'path-mutated', { path, index: this.index });
207
+ }
208
+ }
209
+ }
210
+ });
156
211
  }
157
212
  });
158
- // }
213
+
159
214
  this.getOwnerForm().registerLazyElement(this);
160
215
 
161
216
  const style = `
@@ -185,6 +240,180 @@ export class FxRepeat extends withDraggability(UIElement, false) {
185
240
  // this.init();
186
241
  }
187
242
 
243
+ disconnectedCallback() {
244
+ document.removeEventListener('deleted', this.handleDeleteHandler, true);
245
+ document.removeEventListener('insert', this.handleInsertHandler, true);
246
+ }
247
+
248
+ get repeatSize() {
249
+ return this.querySelectorAll(':scope > fx-repeatitem').length;
250
+ }
251
+
252
+ set repeatSize(size) {
253
+ this.size = size;
254
+ }
255
+
256
+ setIndex(index) {
257
+ // console.log('new repeat index ', index);
258
+ this.index = index;
259
+ const rItems = this.querySelectorAll(':scope > fx-repeatitem');
260
+ this.applyIndex(rItems[this.index - 1]);
261
+
262
+ // trying to do without
263
+ // this.getOwnerForm().refresh({ reason: 'index-function', elementLocalnamesWithChanges: [] });
264
+ }
265
+
266
+ applyIndex(repeatItem) {
267
+ this._removeIndexMarker();
268
+ if (repeatItem) {
269
+ repeatItem.setAttribute('repeat-index', '');
270
+ }
271
+ }
272
+
273
+ get index() {
274
+ return parseInt(this.getAttribute('index'), 10);
275
+ }
276
+
277
+ set index(idx) {
278
+ this.setAttribute('index', idx);
279
+ }
280
+
281
+ _getRef() {
282
+ return this.getAttribute('ref');
283
+ }
284
+
285
+ _createModelItemsRecursively(parentNode, parentModelItem) {
286
+ const parentWithDewey = parentModelItem?.path || null; // e.g. $default/AllowanceCharge[2]_1
287
+ const parentBase = parentWithDewey ? parentWithDewey.replace(/_\d+$/, '') : null; // e.g. $default/AllowanceCharge[2]
288
+
289
+ // Robust Dewey rewrite that tolerates $inst vs instance('inst') forms
290
+ const __applyDeweyRewrite = mi => {
291
+ if (!mi || typeof mi.path !== 'string' || !parentModelItem?.path) return;
292
+
293
+ const pWith = parentModelItem.path; // e.g. $default/AllowanceCharge[2]_1 or instance('default')/AllowanceCharge[2]_1
294
+ const opMatch = pWith.match(/_(\d+)$/);
295
+ if (!opMatch) return;
296
+ const op = opMatch[1];
297
+
298
+ // Normalize to $name/ and strip _n on parent; normalize child for prefix test only
299
+ const toDollar = s => s.replace(/^instance\('([^']+)'\)\//, (_m, g1) => `$${g1}/`);
300
+ const parentBaseNorm = toDollar(pWith).replace(/_\d+$/, ''); // $default/AllowanceCharge[2]
301
+ const childNorm = toDollar(mi.path);
302
+
303
+ if (!childNorm.startsWith(parentBaseNorm)) return; // unrelated subtree
304
+
305
+ // Preserve original style of child's instance prefix
306
+ const childUsesInstanceFn = /^instance\('/.test(mi.path);
307
+ const parentBaseInChildStyle = childUsesInstanceFn
308
+ ? parentBaseNorm.replace(/^\$([A-Za-z0-9_-]+)\//, `instance('$1')/`)
309
+ : parentBaseNorm;
310
+
311
+ // If already suffixed for this parent, nothing to do
312
+ if (mi.path.startsWith(`${parentBaseInChildStyle}_`)) return;
313
+
314
+ // Inject _op immediately after the parent base segment
315
+ mi.path = `${parentBaseInChildStyle}_${op}${mi.path.slice(parentBaseInChildStyle.length)}`;
316
+ };
317
+
318
+ Array.from(parentNode.children).forEach(child => {
319
+ const nextParentMI = parentModelItem;
320
+
321
+ // Skip native/embedded widgets that may carry a 'ref' but are UI only
322
+ const isWidgetEl =
323
+ child &&
324
+ ((child.classList && child.classList.contains('widget')) ||
325
+ (typeof Fore !== 'undefined' && Fore.isWidget && Fore.isWidget(child)) ||
326
+ (child.tagName &&
327
+ ['INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'DATALIST'].includes(child.tagName)));
328
+
329
+ if (!isWidgetEl && child.hasAttribute('ref')) {
330
+ const ref = child.getAttribute('ref').trim();
331
+ if (ref && ref !== '.') {
332
+ // Evaluate the FULL ref once — this yields the terminal (last) node(s)
333
+ let node = evaluateXPath(ref, parentModelItem.node, this);
334
+ if (Array.isArray(node)) node = node[0];
335
+
336
+ if (node) {
337
+ let modelItem = this.getModel().getModelItem(node);
338
+ if (!modelItem) {
339
+ // Create a ModelItem only for the final node; children never get their own opNum
340
+ modelItem = FxBind.createModelItem(ref, node, child, null);
341
+ modelItem.parentModelItem = parentModelItem;
342
+ this.getModel().registerModelItem(modelItem);
343
+ }
344
+
345
+ // Always apply Dewey rewrite (handles both $inst and instance('inst') forms)
346
+ __applyDeweyRewrite(modelItem);
347
+
348
+ child.nodeset = node;
349
+ if (child.attachObserver) child.attachObserver();
350
+ }
351
+ }
352
+ }
353
+
354
+ // Recurse into non-widget subtrees
355
+ if (!isWidgetEl) this._createModelItemsRecursively(child, nextParentMI);
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Removes the repeat item corresponding to a deleted node.
361
+ * Cleans up its observers and notifies the parent form.
362
+ * @param {Node} node - The deleted node
363
+ */
364
+ removeRepeatItemForNode(node) {
365
+ const index = this.nodeset.indexOf(node);
366
+ if (index === -1) return;
367
+
368
+ const repeatItem = this.querySelector(`fx-repeatitem:nth-of-type(${index + 1})`);
369
+ if (repeatItem) {
370
+ this.removeChild(repeatItem);
371
+ this.getOwnerForm().addToBatchedNotifications(this);
372
+ }
373
+
374
+ // Remove the node from the nodeset
375
+ this.nodeset.splice(index, 1);
376
+ }
377
+
378
+ handleDelete(deleted) {
379
+ console.log('handleDelete', deleted);
380
+ // grab the current repeat items (tweak selector if yours differs)
381
+ /**
382
+ * @type {import('./fx-repeatitem.js').FxRepeatitem[]}
383
+ */
384
+ const items = Array.from(
385
+ this.querySelectorAll(
386
+ ':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
387
+ ),
388
+ );
389
+
390
+ this._evalNodeset();
391
+
392
+ const indexToRemove = items.findIndex(item => item.nodeset === deleted);
393
+ if (indexToRemove === -1) {
394
+ return;
395
+ }
396
+ const itemToRemove = items[indexToRemove];
397
+ itemToRemove.remove();
398
+
399
+ // If the list is now empty, clear selection (0). Adjust if you prefer 1-based only.
400
+ const newLength = this.querySelectorAll(
401
+ ':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
402
+ ).length;
403
+
404
+ let nextIndex = indexToRemove + 1; // 1-based index at the deleted slot
405
+ if (newLength === 0) {
406
+ nextIndex = 0; // nothing left; clear selection
407
+ } else if (nextIndex > newLength) {
408
+ nextIndex = newLength; // deleted the last one; move to new last
409
+ }
410
+
411
+ this.setIndex(nextIndex);
412
+ }
413
+
414
+ /**
415
+ * @returns {import('./fx-repeatitem.js').FxRepeatitem}
416
+ */
188
417
  _createNewRepeatItem() {
189
418
  const newItem = document.createElement('fx-repeatitem');
190
419
 
@@ -223,7 +452,7 @@ export class FxRepeat extends withDraggability(UIElement, false) {
223
452
  // console.log('##### inscope ', inscope);
224
453
  // console.log('##### ref ', this.ref);
225
454
  // now we got a nodeset and attach MutationObserver to it
226
-
455
+ if (!inscope) return;
227
456
  if (this.mutationObserver && inscope.nodeName) {
228
457
  this.mutationObserver.observe(inscope, {
229
458
  childList: true,
@@ -251,19 +480,12 @@ export class FxRepeat extends withDraggability(UIElement, false) {
251
480
  }
252
481
 
253
482
  async refresh(force) {
254
- // console.group('fx-repeat.refresh on', this.id);
483
+ console.log('🔄 fx-repeat.refresh on', this.id);
255
484
 
256
485
  if (!this.inited) this.init();
257
486
  // console.time('repeat-refresh', this);
258
487
  this._evalNodeset();
259
488
 
260
- // ### register ourselves as boundControl
261
- /*
262
- const modelItem = this.getModelItem();
263
- if (!modelItem.boundControls.includes(this)) {
264
- modelItem.boundControls.push(this);
265
- }
266
- */
267
489
  // console.log('repeat refresh nodeset ', this.nodeset);
268
490
  // console.log('repeatCount', this.repeatCount);
269
491
 
@@ -297,6 +519,7 @@ export class FxRepeat extends withDraggability(UIElement, false) {
297
519
  const newItem = this._createNewRepeatItem();
298
520
 
299
521
  this.appendChild(newItem);
522
+
300
523
  this._initVariables(newItem);
301
524
 
302
525
  newItem.nodeset = this.nodeset[position - 1];
@@ -308,6 +531,8 @@ export class FxRepeat extends withDraggability(UIElement, false) {
308
531
 
309
532
  // Tell the owner form we might have new template expressions here
310
533
  this.getOwnerForm().scanForNewTemplateExpressionsNextRefresh();
534
+
535
+ newItem.refresh(true);
311
536
  }
312
537
  }
313
538
 
@@ -318,11 +543,15 @@ export class FxRepeat extends withDraggability(UIElement, false) {
318
543
 
319
544
  if (item.nodeset !== this.nodeset[position]) {
320
545
  item.nodeset = this.nodeset[position];
546
+ if (this.getOwnerForm().createNodes) {
547
+ this.getOwnerForm().initData(item);
548
+ }
321
549
  }
322
550
  }
323
551
 
324
- // Fore.refreshChildren(clone,true);
552
+ // Fore.refreshChildren(clone, true);
325
553
  const fore = this.getOwnerForm();
554
+ // if (!fore.lazyRefresh || force) {
326
555
  if (!fore.lazyRefresh || force) {
327
556
  // Turn the possibly conditional force refresh into a forced one: we changed our children
328
557
  Fore.refreshChildren(this, force);
@@ -372,7 +601,6 @@ export class FxRepeat extends withDraggability(UIElement, false) {
372
601
  }
373
602
 
374
603
  _initTemplate() {
375
- this.template = this.querySelector('template');
376
604
  // console.log('### init template for repeat ', this.id, this.template);
377
605
  // todo: this.dropTarget not needed?
378
606
  this.dropTarget = this.template.getAttribute('drop-target');
@@ -415,11 +643,12 @@ export class FxRepeat extends withDraggability(UIElement, false) {
415
643
  if (repeatItem.index === 1) {
416
644
  this.applyIndex(repeatItem);
417
645
  }
418
- // console.log('*********repeat item created', repeatItem.nodeset)
646
+ // console.log('*********repeat item created', repeatItem.nodeset);
419
647
  Fore.dispatch(this, 'item-created', { nodeset: repeatItem.nodeset, pos: index + 1 });
420
648
  this._initVariables(repeatItem);
421
649
  });
422
650
  }
651
+
423
652
  clearTextValues(node) {
424
653
  if (!node) return;
425
654
 
@@ -454,12 +683,30 @@ export class FxRepeat extends withDraggability(UIElement, false) {
454
683
  })(newRepeatItem);
455
684
  }
456
685
 
686
+ /*
457
687
  _clone() {
458
688
  // const content = this.template.content.cloneNode(true);
459
689
  this.template = this.shadowRoot.querySelector('template');
460
690
  const content = this.template.content.cloneNode(true);
461
691
  return document.importNode(content, true);
462
692
  }
693
+ */
694
+
695
+ _clone() {
696
+ // Prefer the cached template set in _initTemplate; fall back to either DOM.
697
+ const tpl =
698
+ this.template ||
699
+ (this.shadowRoot && this.shadowRoot.querySelector('template')) ||
700
+ this.querySelector('template');
701
+
702
+ if (!tpl) {
703
+ console.error(`[fx-repeat] ${this.id || ''}: no <template> found when cloning`);
704
+ return document.createDocumentFragment();
705
+ }
706
+
707
+ const content = tpl.content.cloneNode(true);
708
+ return document.importNode(content, true);
709
+ }
463
710
 
464
711
  _removeIndexMarker() {
465
712
  Array.from(this.children).forEach(item => {
@@ -1,6 +1,7 @@
1
1
  import { Fore } from '../fore.js';
2
- import ForeElementMixin from '../ForeElementMixin.js';
2
+ // import ForeElementMixin from '../ForeElementMixin.js';
3
3
  import { withDraggability } from '../withDraggability.js';
4
+ import { UIElement } from './UIElement.js';
4
5
 
5
6
  /**
6
7
  * `fx-repeat`
@@ -9,9 +10,9 @@ import { withDraggability } from '../withDraggability.js';
9
10
  * @customElement
10
11
  * @demo demo/index.html
11
12
  *
12
- * @extends {ForeElementMixin}
13
+ * @extends {UIElement}
13
14
  */
14
- export class FxRepeatitem extends withDraggability(ForeElementMixin, true) {
15
+ export class FxRepeatitem extends withDraggability(UIElement, true) {
15
16
  static get properties() {
16
17
  return {
17
18
  ...super.properties,
@@ -32,6 +33,8 @@ export class FxRepeatitem extends withDraggability(ForeElementMixin, true) {
32
33
  this.attachShadow({ mode: 'open', delegatesFocus: true });
33
34
 
34
35
  this.dropTarget = null;
36
+ // TODO: rename to position?
37
+ this.index = -1;
35
38
  }
36
39
 
37
40
  connectedCallback() {
@@ -72,7 +75,7 @@ export class FxRepeatitem extends withDraggability(ForeElementMixin, true) {
72
75
  }
73
76
  */
74
77
 
75
- _dispatchIndexChange() {
78
+ async _dispatchIndexChange() {
76
79
  /**
77
80
  * @type {import('./fx-repeat.js').FxRepeat}
78
81
  */
@@ -81,31 +84,31 @@ export class FxRepeatitem extends withDraggability(ForeElementMixin, true) {
81
84
  // The index did not really change if it did not change :wink:
82
85
  return;
83
86
  }
84
- this.dispatchEvent(
87
+ await this.dispatchEvent(
85
88
  new CustomEvent('item-changed', {
86
89
  composed: false,
87
90
  bubbles: true,
88
91
  detail: { item: this, index: this.index },
89
92
  }),
90
93
  );
94
+
95
+ // Refresh after all of the listeners for that item-changed have had their turn to update!
96
+ this.getOwnerForm().refresh();
91
97
  }
92
98
 
93
- refresh(force) {
94
- this.modelItem = this.getModelItem();
95
- // ### register ourselves as boundControl
96
- if (!this.modelItem.boundControls.includes(this)) {
97
- this.modelItem.boundControls.push(this);
98
-
99
- if (this.modelItem && !this.modelItem.relevant) {
100
- this.removeAttribute('relevant');
101
- this.setAttribute('nonrelevant', '');
102
- } else {
103
- this.removeAttribute('nonrelevant');
104
- this.setAttribute('relevant', '');
105
- }
99
+ async refresh(force = false) {
100
+ // this.modelItem = this.getModelItem();
101
+ this.attachObserver();
102
+ // console.log('🔄 repeatitem modelitem', this.getModelItem());
103
+
104
+ if (this.modelItem && !this.modelItem.relevant) {
105
+ this.removeAttribute('relevant');
106
+ this.setAttribute('nonrelevant', '');
107
+ } else {
108
+ this.removeAttribute('nonrelevant');
109
+ this.setAttribute('relevant', '');
106
110
  }
107
- // Always recurse for these refreshes, especially when forced
108
- Fore.refreshChildren(this, force);
111
+ await Fore.refreshChildren(this, force);
109
112
  }
110
113
  }
111
114
 
@@ -48,7 +48,7 @@ class FxSwitch extends FxContainer {
48
48
 
49
49
  async refresh(force) {
50
50
  super.refresh(force);
51
- // console.log('refresh on switch ');
51
+ console.log('🔄 fx-switch refresh', force);
52
52
  if (this.cases.length === 0) {
53
53
  this.cases = Array.from(this.querySelectorAll(':scope > fx-case'));
54
54
  }
@@ -65,7 +65,7 @@ class FxSwitch extends FxContainer {
65
65
  }
66
66
 
67
67
  _dispatchEvents() {
68
- if(this.selectedCase === this.formerCase) return;
68
+ if (this.selectedCase === this.formerCase) return;
69
69
  if (this.formerCase && this.formerCase !== this.selectedCase) {
70
70
  Fore.dispatch(this.formerCase, 'deselect', {});
71
71
  }
@@ -75,7 +75,7 @@ class FxSwitch extends FxContainer {
75
75
  }
76
76
 
77
77
  _resetVisited() {
78
- if(this.visitedResetted) return;
78
+ if (this.visitedResetted) return;
79
79
 
80
80
  const visited = this.selectedCase.querySelectorAll('.visited');
81
81
  Array.from(visited).forEach(v => {
@@ -95,6 +95,8 @@ class FxSwitch extends FxContainer {
95
95
  this.toggle(caseElem);
96
96
  }
97
97
  });
98
+
99
+ this.attachObserver();
98
100
  }
99
101
 
100
102
  /**