@jinntec/fore 2.5.0 → 2.6.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.
@@ -0,0 +1,91 @@
1
+ import ForeElementMixin from '../ForeElementMixin.js';
2
+ import { Fore } from '../fore.js';
3
+
4
+ export class UIElement extends ForeElementMixin {
5
+ constructor() {
6
+ super();
7
+ }
8
+
9
+ connectedCallback() {
10
+ super.connectedCallback();
11
+ this.ondemand = this.hasAttribute('on-demand') ? true : false;
12
+ this.wasOnDemandInitially = this.ondemand;
13
+ if (this.ondemand) {
14
+ this.addEventListener('show-control', () => {
15
+ this.removeAttribute('on-demand');
16
+ });
17
+ this.addTrashIcon();
18
+ }
19
+ }
20
+
21
+ activate() {
22
+ console.log('UIElement.activate() called');
23
+ this.removeAttribute('on-demand');
24
+ this.style.display = '';
25
+ if (this.isBound()) {
26
+ this.refresh(true);
27
+ }
28
+ Fore.dispatch(this, 'show-group', {});
29
+ }
30
+
31
+ attributeChangedCallback(name, _oldValue, newValue) {
32
+ if (name === 'on-demand') {
33
+ this.ondemand = newValue !== null;
34
+ if (!newValue && !this.wasOnDemandInitially) {
35
+ this.removeTrashIcon();
36
+ } else {
37
+ this.wasOnDemandInitially = true;
38
+ this.addTrashIcon();
39
+ }
40
+ }
41
+ }
42
+
43
+ static get observedAttributes() {
44
+ return ['on-demand'];
45
+ }
46
+
47
+ addTrashIcon() {
48
+ // Only show icon if explicitly marked by control-menu
49
+
50
+ if (!this.closest('[show-icon]')) return;
51
+
52
+ // const wrapper = this.shadowRoot.querySelector('.wrapper');
53
+ // if (!wrapper || wrapper.querySelector('.trash')) return;
54
+ const trash = this.querySelector('.trash');
55
+ if (trash) return;
56
+ const icon = document.createElement('span');
57
+ // icon.innerHTML = '🗑'; // trash icon
58
+ icon.innerHTML = `
59
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none"
60
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
61
+ stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
62
+ <path d="M17.94 17.94C16.13 19.12 14.13 20 12 20C7 20 2.73 15.88 1 12C1.6 10.66 2.43 9.47 3.46 8.48M10.58 10.58C10.21 11.01 10 11.5 10 12C10 13.11 10.89 14 12 14C12.5 14 12.99 13.79 13.42 13.42M6.53 6.53C7.87 5.54 9.39 5 12 5C17 5 21.27 9.12 23 12C22.4 13.34 21.57 14.53 20.54 15.52M1 1L23 23"/>
63
+ </svg>
64
+ `;
65
+
66
+ icon.classList.add('trash');
67
+ icon.setAttribute('title', 'Hide');
68
+ // icon.setAttribute('part', 'trash');
69
+ icon.style.cursor = 'pointer';
70
+ icon.style.marginLeft = '0.5em';
71
+
72
+ icon.addEventListener('click', e => {
73
+ e.stopPropagation();
74
+ this.setAttribute('on-demand', 'true');
75
+ this.style.display = 'none';
76
+ document.dispatchEvent(new CustomEvent('update-control-menu'));
77
+ Fore.dispatch(this, 'hide-control', {});
78
+ });
79
+
80
+ this.appendChild(icon);
81
+ }
82
+
83
+ removeTrashIcon() {
84
+ debugger;
85
+ const icon = this.querySelector('.trash');
86
+ if (icon) icon.remove();
87
+ }
88
+ }
89
+ if (!customElements.get('ui-element')) {
90
+ customElements.define('ui-element', UIElement);
91
+ }
@@ -1,9 +1,9 @@
1
1
  import '../fx-model.js';
2
- import ForeElementMixin from '../ForeElementMixin.js';
3
2
  import { ModelItem } from '../modelitem.js';
4
3
  import { Fore } from '../fore.js';
5
4
  import getInScopeContext from '../getInScopeContext.js';
6
5
  import { evaluateXPathToFirstNode } from '../xpath-evaluation.js';
6
+ import { UIElement } from './UIElement.js';
7
7
 
8
8
  function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
9
9
  if (oldNodeValue === null) {
@@ -13,7 +13,7 @@ function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
13
13
  if the oldControlValue is null we know the widget is used for the first time and is not considered
14
14
  a value change.
15
15
  */
16
- if(oldControlValue === null) return false;
16
+ if (oldControlValue === null) return false;
17
17
 
18
18
  if (newControlValue && oldControlValue && newControlValue.nodeType && oldControlValue.nodeType) {
19
19
  return newControlValue.outerHTML !== oldControlValue.outerHTML;
@@ -31,7 +31,7 @@ function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
31
31
  * is a general base class for control elements.
32
32
  *
33
33
  */
34
- export default class AbstractControl extends ForeElementMixin {
34
+ export default class AbstractControl extends UIElement {
35
35
  constructor() {
36
36
  super();
37
37
  this.value = null;
@@ -41,6 +41,7 @@ export default class AbstractControl extends ForeElementMixin {
41
41
  this.widget = null;
42
42
  this.visited = false;
43
43
  this.force = false;
44
+ this.ondemand = false;
44
45
  // this.attachShadow({ mode: 'open' });
45
46
  }
46
47
 
@@ -54,11 +55,13 @@ export default class AbstractControl extends ForeElementMixin {
54
55
  */
55
56
  async refresh(force) {
56
57
  if (force) this.force = true;
57
- // console.log('### AbstractControl.refresh on : ', this);
58
58
 
59
59
  // Save the old value of this control. this may be the stringified version, contrast to the node in `nodeset`
60
60
  const oldValue = this.value;
61
61
 
62
+ // if (this.ondemand && !this.value) return;
63
+ // console.log('### AbstractControl.refresh on : ', this);
64
+
62
65
  // if(this.repeated) return
63
66
  if (this.isNotBound()) return;
64
67
 
@@ -170,14 +173,14 @@ export default class AbstractControl extends ForeElementMixin {
170
173
  // if oldVal is null we haven't received a concrete value yet
171
174
 
172
175
  if (!(this.localName === 'fx-control' || this.localName === 'fx-upload')) return;
173
- if (isDifferent(this.oldVal, oldValue, this.value)) {
176
+ if (isDifferent(this.oldVal, oldValue, this.value)) {
174
177
  const model = this.getModel();
175
178
  Fore.dispatch(this, 'value-changed', {
176
179
  path: this.modelItem.path,
177
180
  value: this.modelItem.value,
178
181
  oldvalue: oldValue,
179
- instanceId:this.modelItem.instanceId,
180
- foreId:this.getOwnerForm().id
182
+ instanceId: this.modelItem.instanceId,
183
+ foreId: this.getOwnerForm().id,
181
184
  });
182
185
  }
183
186
  }
@@ -1,12 +1,12 @@
1
1
  import '../fx-model.js';
2
- import ForeElementMixin from '../ForeElementMixin.js';
2
+ import { UIElement } from './UIElement.js';
3
3
 
4
4
  /**
5
5
  * `fx-container` -
6
6
  * is a general class for container elements.
7
7
  *
8
8
  */
9
- export class FxContainer extends ForeElementMixin {
9
+ export class FxContainer extends UIElement {
10
10
  static get properties() {
11
11
  return {
12
12
  ...super.properties,
@@ -24,6 +24,8 @@ export class FxContainer extends ForeElementMixin {
24
24
  }
25
25
 
26
26
  connectedCallback() {
27
+ super.connectedCallback();
28
+
27
29
  this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
28
30
  const style = `
29
31
  :host {
@@ -41,7 +43,7 @@ export class FxContainer extends ForeElementMixin {
41
43
  </style>
42
44
  ${html}
43
45
  `;
44
- if(this.ref !== ''){
46
+ if (this.ref !== '') {
45
47
  this.getOwnerForm().registerLazyElement(this);
46
48
  }
47
49
 
@@ -0,0 +1,198 @@
1
+ import XfAbstractControl from './abstract-control.js';
2
+
3
+ /**
4
+ * This class finds and lists all elements with an 'on-demand' attribute and offers them
5
+ * in a popup list for activation. 'on-demand' is not a state like 'relevant' but just
6
+ * shows/hides controls on demand. The controls still behave as usual otherwise.
7
+ *
8
+ *
9
+ */
10
+ export class FxControlMenu extends XfAbstractControl {
11
+ connectedCallback() {
12
+ this.attachShadow({ mode: 'open' });
13
+ this.selectExpr = this.getAttribute('select');
14
+
15
+ const style = `
16
+ :host {
17
+ display: inline-block;
18
+ position: relative;
19
+ }
20
+
21
+ .menu {
22
+ display: none;
23
+ position: absolute;
24
+ top: 100%;
25
+ left: 0;
26
+ z-index: 10;
27
+ background: white;
28
+ border: 1px solid #ccc;
29
+ padding: 0.5em;
30
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
31
+ min-width: 10em;
32
+ white-space:nowrap;
33
+ }
34
+
35
+ .menu.visible {
36
+ display: block;
37
+ }
38
+
39
+ .menu a {
40
+ display: block;
41
+ padding: 0.25em 0.5em;
42
+ text-decoration: none;
43
+ color: black;
44
+ cursor: pointer;
45
+ }
46
+
47
+ .menu a:hover {
48
+ background-color: #eee;
49
+ }
50
+ `;
51
+
52
+ this.shadowRoot.innerHTML = `
53
+ <style>${style}</style>
54
+ <slot></slot>
55
+ <div class="menu" part="menu"></div>
56
+ `;
57
+
58
+ this.menuEl = this.shadowRoot.querySelector('.menu');
59
+
60
+ // Slotted button click
61
+ const slot = this.shadowRoot.querySelector('slot');
62
+ slot.addEventListener('slotchange', () => {
63
+ const nodes = slot.assignedNodes({ flatten: true });
64
+ const button = nodes.find(
65
+ node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON',
66
+ );
67
+ if (button) {
68
+ button.addEventListener('click', e => {
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ this.updateMenu();
72
+ this.menuEl.classList.toggle('visible');
73
+ });
74
+ }
75
+ });
76
+
77
+ // Update menu on custom event
78
+ document.addEventListener('update-control-menu', () => {
79
+ this.updateMenu();
80
+ });
81
+
82
+ // Close on outside click
83
+ document.addEventListener('click', e => {
84
+ const inside = this.contains(e.target) || this.shadowRoot.contains(e.target);
85
+ if (!inside) {
86
+ this.menuEl.classList.remove('visible');
87
+ }
88
+ });
89
+
90
+ // Close on Escape
91
+ document.addEventListener('keydown', e => {
92
+ if (e.key === 'Escape') {
93
+ this.menuEl.classList.remove('visible');
94
+ }
95
+ });
96
+
97
+ if (this.getAttribute('mode') === 'hide-on-empty') {
98
+ this.getOwnerForm().addEventListener('ready', () => {
99
+ const container = document.querySelector(this.selectExpr);
100
+ if (!container) return;
101
+
102
+ const widgets = container.querySelectorAll('.widget');
103
+ widgets.forEach(widget => {
104
+ const value = widget.value?.trim();
105
+ const control = widget.closest('fx-control');
106
+ if (control && (value === '' || value == null)) {
107
+ control.setAttribute('on-demand', 'true');
108
+ }
109
+ });
110
+
111
+ // After marking empty controls, update the menu
112
+ this.updateMenu();
113
+ });
114
+ }
115
+
116
+ const container = document.querySelector(this.selectExpr);
117
+ container?.addEventListener('show-control', event => {
118
+ this.updateMenu();
119
+ });
120
+
121
+ this.updateMenu();
122
+ }
123
+
124
+ updateMenu() {
125
+ const container = document.querySelector(this.selectExpr);
126
+ if (!container) return;
127
+
128
+ let targets = [];
129
+
130
+ if (container.hasAttribute('on-demand')) {
131
+ if (container.nodeName === 'FX-REPEAT') {
132
+ // If it's an <fx-repeat> with on-demand, use only the container
133
+ targets = [container];
134
+ } else {
135
+ // If it's not <fx-repeat>, include container and inner [on-demand] targets
136
+ targets = [container, ...container.querySelectorAll('[on-demand]')];
137
+ }
138
+ } else {
139
+ // If container is not on-demand, only look for inner [on-demand]
140
+ targets = Array.from(container.querySelectorAll('[on-demand]'));
141
+ }
142
+ this._currentTargets = targets;
143
+ this.menuEl.innerHTML = ''; // Clear menu
144
+
145
+ // Find the slotted button
146
+ const slot = this.shadowRoot.querySelector('slot');
147
+ const assignedNodes = slot.assignedNodes({ flatten: true });
148
+ const button = assignedNodes.find(
149
+ node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON',
150
+ );
151
+
152
+ if (button) {
153
+ button.disabled = targets.length === 0;
154
+ }
155
+
156
+ if (targets.length === 0) {
157
+ this.menuEl.classList.remove('visible');
158
+ return;
159
+ }
160
+
161
+ targets.forEach((el, index) => {
162
+ let label = el.getAttribute('aria-label');
163
+ if (!label) {
164
+ label = el.querySelector('label')?.textContent.trim() || `Item ${index + 1}`;
165
+ }
166
+ if (!label) {
167
+ console.warn(
168
+ 'no label found - cannot create menu entry for ',
169
+ el,
170
+ ' - please add aria-label or label element to control',
171
+ );
172
+ }
173
+ const item = document.createElement('a');
174
+ item.href = '#';
175
+ item.textContent = label;
176
+
177
+ item.addEventListener('click', e => {
178
+ e.preventDefault();
179
+ if (typeof el.activate === 'function') {
180
+ el.activate();
181
+ }
182
+
183
+ this.menuEl.classList.remove('visible');
184
+
185
+ // Wait one frame to let DOM updates (like on-demand removal) take effect
186
+ requestAnimationFrame(() => {
187
+ this.updateMenu();
188
+ });
189
+ });
190
+
191
+ this.menuEl.appendChild(item);
192
+ });
193
+ }
194
+ }
195
+
196
+ if (!customElements.get('fx-control-menu')) {
197
+ customElements.define('fx-control-menu', FxControlMenu);
198
+ }
@@ -56,7 +56,7 @@ export default class FxControl extends XfAbstractControl {
56
56
  };
57
57
  }
58
58
 
59
- _getValueFromHtmlDom() {
59
+ _getValueOfWidget() {
60
60
  if (this.valueProp === 'selectedOptions') {
61
61
  // We have multiple! Just return that as space-separated for now
62
62
  return [...this.widget.selectedOptions].map(option => option.value).join(' ');
@@ -76,10 +76,25 @@ export default class FxControl extends XfAbstractControl {
76
76
  : 'blur';
77
77
  this.label = this.hasAttribute('label') ? this.getAttribute('label') : null;
78
78
  const style = `
79
- :host{
80
- display:inline-block;
81
- }
82
- `;
79
+ :host {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 0.4em;
83
+ position: relative;
84
+ }
85
+ .trash {
86
+ position: absolute;
87
+ right:0;
88
+ top:0;
89
+ cursor: pointer;
90
+ color: #888;
91
+ font-size: 0.65rem;
92
+ align-self: center;
93
+ }
94
+ .trash:hover {
95
+ color: red;
96
+ }
97
+ `;
83
98
 
84
99
  this.credentials = this.hasAttribute('credentials')
85
100
  ? this.getAttribute('credentials')
@@ -136,7 +151,7 @@ export default class FxControl extends XfAbstractControl {
136
151
  // console.info('handling Event:', event.type, listenOn);
137
152
  // Cancel the default action, if needed
138
153
  event.preventDefault();
139
- this.setValue(this._getValueFromHtmlDom());
154
+ this.setValue(this._getValueOfWidget());
140
155
  }
141
156
  });
142
157
  this.updateEvent = 'blur'; // needs to be registered too
@@ -149,19 +164,19 @@ export default class FxControl extends XfAbstractControl {
149
164
  () => {
150
165
  // console.log('eventlistener ', this.updateEvent);
151
166
  // console.info('handling Event:', event.type, listenOn);
152
- this.setValue(this._getValueFromHtmlDom());
167
+ this.setValue(this._getValueOfWidget());
153
168
  },
154
169
  this.debounceDelay,
155
170
  ),
156
171
  );
157
172
  } else {
158
173
  listenOn.addEventListener(this.updateEvent, event => {
159
- this.setValue(this._getValueFromHtmlDom());
174
+ this.setValue(this._getValueOfWidget());
160
175
  });
161
176
  listenOn.addEventListener(
162
177
  'blur',
163
178
  event => {
164
- this.setValue(this._getValueFromHtmlDom());
179
+ this.setValue(this._getValueOfWidget());
165
180
  },
166
181
  { once: true },
167
182
  );
@@ -185,7 +200,22 @@ export default class FxControl extends XfAbstractControl {
185
200
  this.template = this.querySelector('template');
186
201
  this.boundInitialized = false;
187
202
  this.static = !!this.widget.hasAttribute('static');
188
- // console.log('template',this.template);
203
+ super.connectedCallback();
204
+ }
205
+
206
+ /**
207
+ * activates a control that uses 'on-demand' attribute
208
+ */
209
+ activate() {
210
+ console.log('fx-control.activate() called');
211
+ this.removeAttribute('on-demand');
212
+ this.style.display = '';
213
+ this.refresh(true);
214
+ Fore.dispatch(this, 'show-control', {});
215
+ // Focus the widget after the control becomes visible
216
+ requestAnimationFrame(() => {
217
+ this.getWidget()?.focus();
218
+ });
189
219
  }
190
220
 
191
221
  _debounce(func, timeout = 300) {
@@ -239,7 +269,7 @@ export default class FxControl extends XfAbstractControl {
239
269
  this.modelItem.boundControls.push(this);
240
270
  }
241
271
 
242
- setval.actionPerformed();
272
+ setval.actionPerformed(false);
243
273
  // this.visited = true;
244
274
  }
245
275
 
@@ -260,9 +290,11 @@ export default class FxControl extends XfAbstractControl {
260
290
  }
261
291
 
262
292
  renderHTML(ref) {
293
+ const showTrash = this.hasAttribute('on-demand');
294
+ const showIcon = this.closest('[show-icon]');
263
295
  return `
264
296
  ${this.label ? `${this.label}` : ''}
265
- <slot></slot>
297
+ <slot></slot>
266
298
  ${
267
299
  this.hasAttribute('as') && this.getAttribute('as') === 'node'
268
300
  ? '<fx-replace id="replace" ref=".">'
@@ -311,6 +343,13 @@ export default class FxControl extends XfAbstractControl {
311
343
  */
312
344
  async updateWidgetValue() {
313
345
  // this._getValueFromHtmlDom() = this.value;
346
+ if (this.value) {
347
+ // Fire and forget this: the value is set right away anyway
348
+ this.setAttribute('has-value', '');
349
+ Fore.dispatch(this, 'show-control');
350
+ } else {
351
+ this.removeAttribute('has-value');
352
+ }
314
353
 
315
354
  let { widget } = this;
316
355
  if (!widget) {
@@ -609,24 +648,29 @@ export default class FxControl extends XfAbstractControl {
609
648
  updateEntry(newEntry, node) {
610
649
  // ### >>> todo: needs rework this code is heavily assuming a select control with 'value' attribute - not generic at all yet.
611
650
 
612
- // if (this.widget.nodeName !== 'SELECT') return;
613
- const valueAttribute = this._getValueAttribute(newEntry);
651
+ const valueAttribute = newEntry.getAttribute('value');
614
652
  if (!valueAttribute) {
615
653
  // Fore.dispatch(this,'warn',{message:'no value attribute specified for template entry.'});
616
654
  return;
617
655
  }
618
656
 
619
- const valueExpr = valueAttribute.value;
657
+ const valueExpr = valueAttribute;
620
658
  const cutted = valueExpr.substring(1, valueExpr.length - 1);
621
659
  const evaluated = evaluateXPathToString(cutted, node, newEntry);
622
- valueAttribute.value = evaluated;
660
+ newEntry.setAttribute('value', evaluated);
623
661
 
624
662
  if (this.value === evaluated) {
625
663
  newEntry.setAttribute('selected', 'selected');
626
664
  }
627
665
 
666
+ if (newEntry.hasAttribute('title')) {
667
+ let titleExpr = newEntry.getAttribute('title');
668
+ titleExpr = titleExpr.substring(1, titleExpr.length - 1);
669
+ const evaluated = evaluateXPathToString(titleExpr, node, newEntry);
670
+ newEntry.setAttribute('title', evaluated);
671
+ }
628
672
  // ### set label
629
- const optionLabel = newEntry.textContent;
673
+ const optionLabel = newEntry.textContent.trim();
630
674
  this.evalLabel(optionLabel, node, newEntry);
631
675
  // ### <<< needs rework
632
676
  }
@@ -61,7 +61,7 @@ export class FxDialog extends HTMLElement {
61
61
  `;
62
62
  }
63
63
 
64
- open() {
64
+ showModal() {
65
65
  window.addEventListener(
66
66
  'keyup',
67
67
  e => {
@@ -75,7 +75,7 @@ export class FxDialog extends HTMLElement {
75
75
  this.classList.add('show');
76
76
  }
77
77
 
78
- async hide() {
78
+ async close() {
79
79
  await Fore.fadeOutElement(this, 400);
80
80
  this.classList.remove('show');
81
81
  }
@@ -95,6 +95,20 @@ class FxGroup extends FxContainer {
95
95
  // context item
96
96
  Fore.refreshChildren(this, !!force);
97
97
  }
98
+
99
+ // todo: this code should go
100
+ /**
101
+ * activates a control that uses 'on-demand' attribute
102
+ */
103
+ activate() {
104
+ console.log('fx-group.activate() called');
105
+ this.removeAttribute('on-demand');
106
+ this.style.display = '';
107
+ if (this.isBound()) {
108
+ this.refresh(true);
109
+ }
110
+ Fore.dispatch(this, 'show-group', {});
111
+ }
98
112
  }
99
113
 
100
114
  if (!customElements.get('fx-group')) {
@@ -6,6 +6,7 @@ import { evaluateXPath } from '../xpath-evaluation.js';
6
6
  import getInScopeContext from '../getInScopeContext.js';
7
7
  import { XPathUtil } from '../xpath-util.js';
8
8
  import { withDraggability } from '../withDraggability.js';
9
+ import { UIElement } from './UIElement.js';
9
10
 
10
11
  // import {DependencyNotifyingDomFacade} from '../DependencyNotifyingDomFacade';
11
12
 
@@ -25,7 +26,7 @@ import { withDraggability } from '../withDraggability.js';
25
26
  * todo: it should be seriously be considered to extend FxContainer instead but needs refactoring first.
26
27
  * @extends {ForeElementMixin}
27
28
  */
28
- export class FxRepeat extends withDraggability(ForeElementMixin, false) {
29
+ export class FxRepeat extends withDraggability(UIElement, false) {
29
30
  static get properties() {
30
31
  return {
31
32
  ...super.properties,
@@ -80,7 +81,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
80
81
  const rItems = this.querySelectorAll(':scope > fx-repeatitem');
81
82
  this.applyIndex(rItems[this.index - 1]);
82
83
 
83
- this.getOwnerForm().refresh({ reason: 'index-function' });
84
+ this.getOwnerForm().refresh({ reason: 'index-function', elementLocalnamesWithChanges: [] });
84
85
  }
85
86
 
86
87
  applyIndex(repeatItem) {
@@ -107,6 +108,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
107
108
  // console.log('connectedCallback',this);
108
109
  // this.display = window.getComputedStyle(this, null).getPropertyValue("display");
109
110
  this.ref = this.getAttribute('ref');
111
+ this.dependencies.addXPath(this.ref);
110
112
  // this.ref = this._getRef();
111
113
  // console.log('### fx-repeat connected ', this.id);
112
114
  this.addEventListener('item-changed', e => {
@@ -171,6 +173,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
171
173
  const html = `
172
174
  <slot name="header"></slot>
173
175
  <slot></slot>
176
+ <slot name="footer"></slot>
174
177
  `;
175
178
  this.shadowRoot.innerHTML = `
176
179
  <style>
@@ -197,7 +200,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
197
200
 
198
201
  init() {
199
202
  // ### there must be a single 'template' child
200
- console.log('##### repeat init ', this.id);
203
+ // console.log('##### repeat init ', this.id);
201
204
  // if(!this.inited) this.init();
202
205
  // does not use this.evalInContext as it is expecting a nodeset instead of single node
203
206
  this._evalNodeset();
@@ -321,6 +324,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
321
324
  // Fore.refreshChildren(clone,true);
322
325
  const fore = this.getOwnerForm();
323
326
  if (!fore.lazyRefresh || force) {
327
+ // Turn the possibly conditional force refresh into a forced one: we changed our children
324
328
  Fore.refreshChildren(this, force);
325
329
  }
326
330
  // this.style.display = 'block';
@@ -400,6 +404,12 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
400
404
 
401
405
  if (this.getOwnerForm().createNodes) {
402
406
  this.getOwnerForm().initData(repeatItem);
407
+ const repeatItemClone = repeatItem.nodeset.cloneNode(true);
408
+ this.clearTextValues(repeatItemClone);
409
+
410
+ // this.createdNodeset = repeatItem.nodeset.cloneNode(true);
411
+ this.createdNodeset = repeatItemClone;
412
+ // console.log('createdNodeset', this.createdNodeset)
403
413
  }
404
414
 
405
415
  if (repeatItem.index === 1) {
@@ -410,6 +420,26 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
410
420
  this._initVariables(repeatItem);
411
421
  });
412
422
  }
423
+ clearTextValues(node) {
424
+ if (!node) return;
425
+
426
+ // Clear text node content
427
+ if (node.nodeType === Node.TEXT_NODE) {
428
+ node.nodeValue = '';
429
+ }
430
+
431
+ // Clear all attribute values
432
+ if (node.nodeType === Node.ELEMENT_NODE) {
433
+ for (const attr of Array.from(node.attributes)) {
434
+ attr.value = ''; // Clear attribute value
435
+ }
436
+ }
437
+
438
+ // Recursively clear child nodes
439
+ for (const child of node.childNodes) {
440
+ this.clearTextValues(child);
441
+ }
442
+ }
413
443
 
414
444
  _initVariables(newRepeatItem) {
415
445
  const inScopeVariables = new Map(this.inScopeVariables);