@jinntec/fore 1.4.0 → 1.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.
Files changed (71) hide show
  1. package/dist/fore-dev.js +2 -36
  2. package/dist/fore-dev.js.map +1 -1
  3. package/dist/fore.js +2 -30
  4. package/dist/fore.js.map +1 -1
  5. package/index.js +13 -0
  6. package/package.json +9 -5
  7. package/resources/fore.css +178 -92
  8. package/src/DependencyNotifyingDomFacade.js +1 -2
  9. package/src/ForeElementMixin.js +31 -5
  10. package/src/actions/abstract-action.js +379 -270
  11. package/src/actions/fx-action.js +0 -1
  12. package/src/actions/fx-append.js +1 -2
  13. package/src/actions/fx-confirm.js +12 -0
  14. package/src/actions/fx-copy.js +0 -1
  15. package/src/actions/fx-delete.js +31 -9
  16. package/src/actions/fx-dispatch.js +19 -5
  17. package/src/actions/fx-hide.js +19 -0
  18. package/src/actions/fx-insert.js +72 -8
  19. package/src/actions/fx-load.js +253 -0
  20. package/src/actions/fx-message.js +22 -7
  21. package/src/actions/fx-refresh.js +11 -1
  22. package/src/actions/fx-reload.js +12 -2
  23. package/src/actions/fx-replace.js +5 -4
  24. package/src/actions/fx-reset.js +48 -0
  25. package/src/actions/fx-return.js +0 -1
  26. package/src/actions/fx-send.js +40 -2
  27. package/src/actions/fx-setfocus.js +25 -7
  28. package/src/actions/fx-setvalue.js +32 -4
  29. package/src/actions/fx-show.js +14 -2
  30. package/src/actions/fx-toggle.js +0 -1
  31. package/src/actions/fx-update.js +9 -0
  32. package/src/events.js +0 -1
  33. package/src/fore.js +119 -63
  34. package/src/functions/common-function.js +28 -0
  35. package/src/fx-bind.js +7 -7
  36. package/src/fx-fore.js +207 -54
  37. package/src/fx-instance.js +55 -17
  38. package/src/fx-model.js +31 -33
  39. package/src/fx-submission.js +50 -47
  40. package/src/getInScopeContext.js +22 -8
  41. package/src/lab/fore-component.js +90 -0
  42. package/src/lab/instance-inspector.js +56 -0
  43. package/src/lab/template.html +16 -0
  44. package/src/relevance.js +27 -1
  45. package/src/tools/adi.js +1056 -0
  46. package/src/tools/fx-action-log.js +662 -0
  47. package/src/tools/fx-devtools.js +444 -0
  48. package/src/tools/fx-dom-inspector.js +609 -0
  49. package/src/tools/fx-json-instance.js +435 -0
  50. package/src/tools/fx-log-item.js +133 -0
  51. package/src/tools/fx-log-settings.js +474 -0
  52. package/src/tools/fx-minimap.js +194 -0
  53. package/src/tools/helpers.js +132 -0
  54. package/src/ui/abstract-control.js +41 -3
  55. package/src/ui/fx-action-log.js +358 -0
  56. package/src/ui/fx-alert.js +0 -1
  57. package/src/ui/fx-container.js +14 -3
  58. package/src/ui/fx-control.js +553 -474
  59. package/src/ui/fx-dialog.js +2 -0
  60. package/src/ui/fx-dom-inspector.js +1255 -0
  61. package/src/ui/fx-group.js +3 -4
  62. package/src/ui/fx-hint.js +2 -4
  63. package/src/ui/fx-inspector.js +5 -6
  64. package/src/ui/fx-items.js +55 -14
  65. package/src/ui/fx-output.js +36 -17
  66. package/src/ui/fx-repeat-attributes.js +409 -0
  67. package/src/ui/fx-repeat.js +12 -6
  68. package/src/ui/fx-switch.js +14 -3
  69. package/src/ui/fx-trigger.js +13 -1
  70. package/src/xpath-evaluation.js +109 -26
  71. package/src/xpath-util.js +55 -1
@@ -1,14 +1,14 @@
1
1
  import XfAbstractControl from './abstract-control.js';
2
2
  import {
3
- evaluateXPath,
4
- evaluateXPathToString,
5
- evaluateXPathToFirstNode,
3
+ evaluateXPath,
4
+ evaluateXPathToString,
5
+ evaluateXPathToFirstNode, resolveId,
6
6
  } from '../xpath-evaluation.js';
7
7
  import getInScopeContext from '../getInScopeContext.js';
8
- import { Fore } from '../fore.js';
8
+ import {Fore} from '../fore.js';
9
9
  import {ModelItem} from "../modelitem.js";
10
10
  import {debounce} from "../events.js";
11
- import {FxModel} from "../fx-model";
11
+ import {FxModel} from "../fx-model.js";
12
12
 
13
13
  const WIDGETCLASS = 'widget';
14
14
 
@@ -34,532 +34,611 @@ function debounce( func, timeout = 300) {
34
34
  }
35
35
  */
36
36
  export default class FxControl extends XfAbstractControl {
37
- constructor() {
38
- super();
39
- this.inited = false;
40
- this.attachShadow({ mode: 'open' });
41
- }
42
-
43
- connectedCallback() {
44
- this.initial = this.hasAttribute('initial') ? this.getAttribute('initial') : null;
45
- this.url = this.hasAttribute('url') ? this.getAttribute('url') : null;
46
- this.loaded = false;
47
- this.initialNode = null;
48
- this.debounceDelay = this.hasAttribute('debounce') ? this.getAttribute('debounce') : null;
49
-
50
- this.updateEvent = this.hasAttribute('update-event')
51
- ? this.getAttribute('update-event')
52
- : 'blur';
53
- this.valueProp = this.hasAttribute('value-prop') ? this.getAttribute('value-prop') : 'value';
54
- this.label = this.hasAttribute('label') ? this.getAttribute('label') : null;
55
- const style = `
37
+ constructor() {
38
+ super();
39
+ this.inited = false;
40
+ this.attachShadow({mode: 'open'});
41
+ }
42
+
43
+ static get properties() {
44
+ return {
45
+ ...XfAbstractControl.properties,
46
+ initial: {
47
+ type: Boolean
48
+ }
49
+ };
50
+ }
51
+
52
+ _getValueFromHtmlDom() {
53
+ if (this.valueProp === 'selectedOptions') {
54
+ // We have multiple! Just return that as space-separated for now
55
+ return [...this.widget.selectedOptions].map(option => option.value).join(' ');
56
+ }
57
+ return this.widget[this.valueProp];
58
+ }
59
+
60
+ connectedCallback() {
61
+ this.initial = this.hasAttribute('initial') ? this.getAttribute('initial') : null;
62
+ this.url = this.hasAttribute('url') ? this.getAttribute('url') : null;
63
+ this.loaded = false;
64
+ this.initialNode = null;
65
+ this.debounceDelay = this.hasAttribute('debounce') ? this.getAttribute('debounce') : null;
66
+
67
+ this.updateEvent = this.hasAttribute('update-event')
68
+ ? this.getAttribute('update-event')
69
+ : 'blur';
70
+ this.label = this.hasAttribute('label') ? this.getAttribute('label') : null;
71
+ const style = `
56
72
  :host{
57
73
  display:inline-block;
58
74
  }
59
75
  `;
60
76
 
61
- this.shadowRoot.innerHTML = `
77
+ this.shadowRoot.innerHTML = `
62
78
  <style>
63
79
  ${style}
64
80
  </style>
65
81
  ${this.renderHTML(this.ref)}
66
82
  `;
67
83
 
68
- this.widget = this.getWidget();
69
- // console.log('widget ', this.widget);
70
- let listenOn = this.widget // default: usually listening on widget
84
+ this.widget = this.getWidget();
71
85
 
72
- if(this.hasAttribute('listen-on')){
73
- const q = this.getAttribute('listen-on');
74
- const target = this.querySelector(q);
75
- if(target){
76
- listenOn = target;
77
- }
78
- }
86
+ this.addEventListener('mousedown', e => {
87
+ // ### prevent mousedown events on all control content that is not the widget or within the widget
88
+ if (!Fore.isWidget(e.target) && !e.target?.classList.contains('fx-hint')) {
89
+ e.preventDefault();
90
+ // e.stopImmediatePropagation();
91
+ }
92
+ this.widget.focus();
93
+ });
94
+
95
+
96
+ const defaultValueProp = this.widget.hasAttribute('multiple') ? 'selectedOptions' : 'value';
97
+ this.valueProp = this.hasAttribute('value-prop') ?
98
+ this.getAttribute('value-prop') :
99
+ defaultValueProp;
79
100
 
80
101
 
81
- this.addEventListener('keyup', event => {
82
- FxModel.dataChanged = true;
83
- });
84
- // ### convenience marker event
85
- if (this.updateEvent === 'enter') {
86
- this.widget.addEventListener('keyup', event => {
87
- if (event.keyCode === 13) {
88
- // console.info('handling Event:', event.type, listenOn);
89
- // Cancel the default action, if needed
90
- event.preventDefault();
91
- this.setValue(this.widget[this.valueProp]);
102
+ // console.log('widget ', this.widget);
103
+ let listenOn = this.widget // default: usually listening on widget
104
+
105
+ if (this.hasAttribute('listen-on')) {
106
+ const q = this.getAttribute('listen-on');
107
+ const target = this.querySelector(q);
108
+ if (target) {
109
+ listenOn = target;
110
+ }
92
111
  }
93
- });
94
- this.updateEvent = 'blur'; // needs to be registered too
95
- }
96
- if (this.debounceDelay) {
97
- listenOn.addEventListener(
98
- this.updateEvent,
99
- debounce(this,() => {
100
- // console.log('eventlistener ', this.updateEvent);
101
- // console.info('handling Event:', event.type, listenOn);
102
- this.setValue(this.widget[this.valueProp]);
103
- }, this.debounceDelay),
104
- );
105
- } else {
106
- listenOn.addEventListener(this.updateEvent, () => {
107
- // console.info('handling Event:', event.type, listenOn);
108
- this.setValue(this.widget[this.valueProp]);
109
- });
110
- }
111
112
 
112
- this.addEventListener('return', e => {
113
- console.log('catched return action on ', this);
114
- console.log('return detail', e.detail);
115
-
116
- console.log('return triggered on ', this);
117
- console.log('this.ref', this.ref);
118
- console.log('current outer instance', this.getInstance());
119
-
120
- console.log(
121
- '???? why ???? current nodeset should point to the node of the outer control',
122
- e.currentTarget.nodeset,
123
- );
124
- console.log(
125
- '???? why ???? current nodeset should point to the node of the outer control',
126
- this.nodeset,
127
- );
128
- const newNodes = e.detail.nodeset;
129
- console.log('new nodeset', newNodes);
130
- console.log('currentTarget', e.currentTarget);
131
- console.log('target', e.target);
132
-
133
- e.stopPropagation();
134
-
135
- this._replaceNode(newNodes);
136
- });
137
-
138
- this.template = this.querySelector('template');
139
- this.boundInitialized = false;
140
- this.static = this.widget.hasAttribute('static')? true:false;
141
- // console.log('template',this.template);
142
- }
143
-
144
- _debounce(func, timeout = 300) {
145
- let timer;
146
- return (...args) => {
147
- const context = this;
148
- clearTimeout(timer);
149
- timer = setTimeout(() => {
150
- func.apply(context, args);
151
- }, timeout);
152
- };
153
- }
154
-
155
- /**
156
- * updates the model with a new value by executing it's `<fx-setvalue>` action.
157
- *
158
- * In case the `as='node'` is given the bound node is replaced with the widgets' value with is
159
- * expected to be a node again.
160
- *
161
- * @param val the new value to be set
162
- */
163
- setValue(val) {
164
- const modelitem = this.getModelItem();
165
-
166
- this.classList.add('visited');
167
-
168
- if (modelitem?.readonly){
169
- console.warn('attempt to change readonly node', modelitem);
170
- return; // do nothing when modelItem is readonly
171
- }
172
113
 
173
- if (this.getAttribute('as') === 'node') {
174
- const widgetValue = this.getWidget().value;
175
- const replace = this.shadowRoot.getElementById('replace');
176
- replace.replace(this.nodeset, this.getWidget().value);
177
- if (modelitem && widgetValue && widgetValue !== modelitem.value) {
178
- modelitem.value = widgetValue;
179
- FxModel.dataChanged = true;
180
- replace.actionPerformed();
181
- }
182
- return;
114
+ this.addEventListener('keyup', () => {
115
+ FxModel.dataChanged = true;
116
+ /*
117
+ if (!this.classList.contains('visited')) {
118
+ this.classList.add('visited');
119
+ }
120
+ */
121
+ });
122
+ // ### convenience marker event
123
+ if (this.updateEvent === 'enter') {
124
+ this.widget.addEventListener('keyup', event => {
125
+ if (event.keyCode === 13) {
126
+ // console.info('handling Event:', event.type, listenOn);
127
+ // Cancel the default action, if needed
128
+ event.preventDefault();
129
+ this.setValue(this._getValueFromHtmlDom());
130
+ }
131
+ });
132
+ this.updateEvent = 'blur'; // needs to be registered too
133
+ }
134
+ if (this.debounceDelay) {
135
+ listenOn.addEventListener(
136
+ this.updateEvent,
137
+ debounce(this, () => {
138
+ // console.log('eventlistener ', this.updateEvent);
139
+ // console.info('handling Event:', event.type, listenOn);
140
+ this.setValue(this._getValueFromHtmlDom());
141
+ }, this.debounceDelay),
142
+ );
143
+ } else {
144
+ listenOn.addEventListener(this.updateEvent, (event) => {
145
+ this.setValue(this._getValueFromHtmlDom());
146
+ });
147
+ listenOn.addEventListener('blur', (event) => {
148
+ this.setValue(this._getValueFromHtmlDom());
149
+ },{once:true});
150
+ }
151
+
152
+ this.addEventListener('return', e => {
153
+ // console.log('catched return action on ', this);
154
+ // console.log('return detail', e.detail);
155
+
156
+ // console.log('return triggered on ', this);
157
+ // console.log('this.ref', this.ref);
158
+ // console.log('current outer instance', this.getInstance());
159
+
160
+ /*
161
+ console.log(
162
+ '???? why ???? current nodeset should point to the node of the outer control',
163
+ e.currentTarget.nodeset,
164
+ );
165
+ console.log(
166
+ '???? why ???? current nodeset should point to the node of the outer control',
167
+ this.nodeset,
168
+ );
169
+ */
170
+ const newNodes = e.detail.nodeset;
171
+ // console.log('new nodeset', newNodes);
172
+ // console.log('currentTarget', e.currentTarget);
173
+ // console.log('target', e.target);
174
+
175
+ e.stopPropagation();
176
+
177
+ this._replaceNode(newNodes);
178
+ });
179
+
180
+ this.widget.addEventListener('focus', () => {
181
+ /*
182
+ if (!this.classList.contains('visited')) {
183
+ this.classList.add('visited');
184
+ }
185
+ */
186
+ });
187
+
188
+ this.template = this.querySelector('template');
189
+ this.boundInitialized = false;
190
+ this.static = !!this.widget.hasAttribute('static');
191
+ // console.log('template',this.template);
183
192
  }
184
- const setval = this.shadowRoot.getElementById('setvalue');
185
- setval.setValue(modelitem, val);
186
193
 
187
- if (this.modelItem instanceof ModelItem && !this.modelItem?.boundControls.includes(this)) {
188
- this.modelItem.boundControls.push(this);
194
+ _debounce(func, timeout = 300) {
195
+ let timer;
196
+ return (...args) => {
197
+ const context = this;
198
+ clearTimeout(timer);
199
+ timer = setTimeout(() => {
200
+ func.apply(context, args);
201
+ }, timeout);
202
+ };
189
203
  }
190
204
 
191
- setval.actionPerformed();
192
- this.visited = true;
193
- }
205
+ /**
206
+ * updates the model with a new value by executing it's `<fx-setvalue>` action.
207
+ *
208
+ * In case the `as='node'` is given the bound node is replaced with the widgets' value with is
209
+ * expected to be a node again.
210
+ *
211
+ * @param val the new value to be set
212
+ */
213
+ setValue(val) {
214
+ const modelitem = this.getModelItem();
215
+
216
+ if(this.getAttribute('class')){
217
+ this.classList.add('visited');
218
+ }else{
219
+ this.setAttribute('class','visited');
220
+ }
221
+
222
+ if (modelitem?.readonly) {
223
+ console.warn('attempt to change readonly node', modelitem);
224
+ return; // do nothing when modelItem is readonly
225
+ }
226
+
227
+ if (this.getAttribute('as') === 'node') {
228
+ const replace = this.shadowRoot.getElementById('replace');
229
+ const widgetValue = this.getWidget()[this.valueProp];
230
+ replace.replace(this.nodeset, widgetValue);
231
+ if (modelitem && widgetValue && widgetValue !== modelitem.value) {
232
+ modelitem.value = widgetValue;
233
+ FxModel.dataChanged = true;
234
+ replace.actionPerformed();
235
+ }
236
+ return;
237
+ }
238
+ const setval = this.shadowRoot.getElementById('setvalue');
239
+ setval.setValue(modelitem, val);
240
+
241
+ if (this.modelItem instanceof ModelItem && !this.modelItem?.boundControls.includes(this)) {
242
+ this.modelItem.boundControls.push(this);
243
+ }
244
+
245
+ setval.actionPerformed();
246
+ // this.visited = true;
247
+ }
194
248
 
195
- _replaceNode(node) {
196
- // Note: clone the node while replacing to prevent the instances to leak through
197
- this.modelItem.node.replaceWith(node.cloneNode(true));
198
- this.getOwnerForm().refresh();
199
- }
249
+ _replaceNode(node) {
250
+ // Note: clone the node while replacing to prevent the instances to leak through
251
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
252
+ this.modelItem.node.nodeValue = node.nodeValue;
253
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
254
+ this.modelItem.node.replaceWith(node.cloneNode(true));
255
+ } else if (node.nodeType === Node.TEXT_NODE) {
256
+ this.modelItem.node.nodeValue = node.textContent;
257
+ } else (
258
+ Fore.dispatch(this, "warn", {'message': 'trying to replace a node that is neither an Attribute, Elemment or Text node'})
259
+ )
260
+ this.getOwnerForm().refresh();
261
+ }
200
262
 
201
- renderHTML(ref) {
202
- return `
263
+ renderHTML(ref) {
264
+ return `
203
265
  ${this.label ? `${this.label}` : ''}
204
266
  <slot></slot>
205
267
  ${
206
- this.hasAttribute('as') && this.getAttribute('as') === 'node'
268
+ this.hasAttribute('as') && this.getAttribute('as') === 'node'
207
269
  ? `<fx-replace id="replace" ref=".">`
208
270
  : `<fx-setvalue id="setvalue" ref="${ref}"></fx-setvalue>`
209
- }
271
+ }
210
272
 
211
273
  `;
212
- }
213
-
214
- /**
215
- * The widget is the actual control being used in the UI e.g. a native input control or any
216
- * other component that presents a control that can be interacted with.
217
- *
218
- * This function returns the widget by querying the children of this control for an element
219
- * with `class="widget"`. If that cannot be found it searches for an native `input` of any type.
220
- * If either cannot be found a `<input type="text">` is created.
221
- *
222
- * @returns {HTMLElement|*}
223
- */
224
- getWidget() {
225
- if (this.widget) return this.widget;
226
- let widget = this.querySelector(`.${WIDGETCLASS}`);
227
- if (!widget) {
228
- widget = this.querySelector('input');
229
274
  }
230
- if (!widget) {
231
- const input = document.createElement('input');
232
- input.classList.add(WIDGETCLASS);
233
- input.setAttribute('type', 'text');
234
- this.appendChild(input);
235
- return input;
236
- }
237
- return widget;
238
- }
239
-
240
- /**
241
- * updates the widget from the modelItem value. During refresh the a control
242
- * evaluates it's binding expression to determine the bound node. The bound node corresponds
243
- * to a modelItem which acts a the state object of a node. The modelItem determines the value
244
- * and the state of the node and set the `value` property of this class.
245
- *
246
- * @returns {Promise<void>}
247
- */
248
- async updateWidgetValue() {
249
- // this.widget[this.valueProp] = this.value;
250
-
251
- let { widget } = this;
252
- if (!widget) {
253
- widget = this;
254
- }
255
- // ### value is bound to checkbox
256
- if (this.valueProp === 'checked') {
257
- if (this.value === 'true') {
258
- widget.checked = true;
259
- } else {
260
- widget.checked = false;
261
- }
262
- return;
275
+
276
+ /**
277
+ * The widget is the actual control being used in the UI e.g. a native input control or any
278
+ * other component that presents a control that can be interacted with.
279
+ *
280
+ * This function returns the widget by querying the children of this control for an element
281
+ * with `class="widget"`. If that cannot be found it searches for an native `input` of any type.
282
+ * If either cannot be found a `<input type="text">` is created.
283
+ *
284
+ * @returns {HTMLElement|*}
285
+ */
286
+ getWidget() {
287
+ if (this.widget) return this.widget;
288
+ let widget = this.querySelector(`.${WIDGETCLASS}`);
289
+ if (!widget) {
290
+ widget = this.querySelector('input');
291
+ if(widget && !widget.classList.contains('widget')){
292
+ widget.classList.add('widget');
293
+ }
294
+ }
295
+ if (!widget) {
296
+ const input = document.createElement('input');
297
+ input.classList.add(WIDGETCLASS);
298
+ input.setAttribute('type', 'text');
299
+ this.appendChild(input);
300
+ return input;
301
+ }
302
+ return widget;
263
303
  }
264
304
 
265
- if (this.hasAttribute('as')) {
266
- const as = this.getAttribute('as');
267
-
268
- // ### when there's an `as=text` attribute serialize nodeset to prettified string
269
- if (as === 'text') {
270
- const serializer = new XMLSerializer();
271
- const pretty = Fore.prettifyXml(serializer.serializeToString(this.nodeset));
272
- widget.value = pretty;
273
- }
274
- if (as === 'node' && this.nodeset !== widget.value) {
275
- // const oldVal = this.nodeset.innerHTML;
276
- const oldVal = this.nodeset;
277
- if (widget.value) {
278
- if (oldVal !== this.widget.value) {
279
- console.log('changed');
280
- widget.value = this.nodeset.cloneNode(true);
305
+ /**
306
+ * updates the widget from the modelItem value. During refresh the a control
307
+ * evaluates it's binding expression to determine the bound node. The bound node corresponds
308
+ * to a modelItem which acts a the state object of a node. The modelItem determines the value
309
+ * and the state of the node and set the `value` property of this class.
310
+ *
311
+ * @returns {Promise<void>}
312
+ */
313
+ async updateWidgetValue() {
314
+ // this._getValueFromHtmlDom() = this.value;
315
+
316
+ let {widget} = this;
317
+ if (!widget) {
318
+ widget = this;
319
+ }
320
+ // ### value is bound to checkbox
321
+ if (this.valueProp === 'checked') {
322
+ if (this.value === 'true') {
323
+ widget.checked = true;
324
+ } else {
325
+ widget.checked = false;
326
+ }
327
+ return;
328
+ }
329
+
330
+ if (this.valueProp === 'selectedOptions') {
331
+ const valueSet = new Set(this.value.split(' '));
332
+ for (const option of [...this.widget.querySelectorAll('option')]) {
333
+ if (valueSet.has(option.value)) {
334
+ option.selected = true;
335
+ } else {
336
+ option.selected = false;
337
+ }
338
+ }
339
+ return;
340
+ }
341
+
342
+ if (this.hasAttribute('as')) {
343
+ const as = this.getAttribute('as');
344
+
345
+ // ### when there's an `as=text` attribute serialize nodeset to prettified string
346
+ if (as === 'text') {
347
+ const serializer = new XMLSerializer();
348
+ const pretty = Fore.prettifyXml(serializer.serializeToString(this.nodeset));
349
+ widget.value = pretty;
350
+ }
351
+ if (as === 'node' && this.nodeset !== widget.value) {
352
+ // const oldVal = this.nodeset.innerHTML;
353
+ const oldVal = this.nodeset;
354
+ if (widget.value) {
355
+ if (oldVal !== this.widget.value) {
356
+ // console.log('changed');
357
+ widget.value = this.nodeset.cloneNode(true);
358
+ return;
359
+ }
360
+ }
361
+
362
+ widget.value = this.nodeset.cloneNode(true);
363
+ // todo: should be more like below but that can cause infinite loop when controll trigger update event due to calling a setter for property
364
+ // widget[this.valueProp] = this.nodeset.cloneNode(true);
365
+ // console.log('passed value to widget', widget.value);
366
+ }
367
+
281
368
  return;
282
- }
283
369
  }
284
370
 
285
- widget.value = this.nodeset.cloneNode(true);
286
- // todo: should be more like below but that can cause infinite loop when controll trigger update event due to calling a setter for property
287
- // widget[this.valueProp] = this.nodeset.cloneNode(true);
288
- // console.log('passed value to widget', widget.value);
289
- }
371
+ // ### when there's a url Fore is used as widget and will be loaded from external file
372
+ if (this.url && !this.loaded) {
373
+ // ### evaluate initial data if necessary
374
+
375
+ if (this.initial) {
376
+ this.initialNode = evaluateXPathToFirstNode(this.initial, this.nodeset, this);
377
+ // console.log('initialNodes', this.initialNode);
378
+ }
379
+
380
+ // ### load the markup from Url
381
+ await this._loadForeFromUrl();
382
+ this.loaded = true;
383
+
384
+ // ### replace default instance of embedded Fore with initial nodes
385
+ // const innerInstance = this.querySelector('fx-instance');
386
+ // console.log('innerInstance',innerInstance);
387
+ return;
388
+ }
290
389
 
291
- return;
390
+ /*
391
+ if(this.url && !this.loaded){
392
+ this._loadForeFromUrl();
393
+ this.loaded=true;
394
+ return;
395
+ }
396
+ */
397
+ if (widget.value !== this.value) {
398
+ widget.value = this.value;
399
+ }
292
400
  }
293
401
 
294
- // ### when there's a url Fore is used as widget and will be loaded from external file
295
- if (this.url && !this.loaded) {
296
- // ### evaluate initial data if necessary
402
+ /**
403
+ * loads an external Fore from an HTML file given by `url` attribute.
404
+ *
405
+ * Will look for the `<fx-fore>` element within the returned HTML file and return that element.
406
+ *
407
+ * If that cannot be found an error is dispatched.
408
+ *
409
+ * todo: dispatch link error
410
+ * @private
411
+ */
412
+ async _loadForeFromUrl() {
413
+ console.info(
414
+ `%cFore is processing URL ${this.url}`,
415
+ "background:#64b5f6; color:white; padding:0.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;",
416
+ );
417
+ try {
418
+ const response = await fetch(this.url, {
419
+ method: 'GET',
420
+ /*
421
+ mode: 'cors',
422
+ credentials: 'include',
423
+ */
424
+ headers: {
425
+ 'Content-Type': 'text/html',
426
+ },
427
+ });
428
+
429
+ const responseContentType = response.headers.get('content-type').toLowerCase();
430
+ // console.log('********** responseContentType *********', responseContentType);
431
+ let data;
432
+ if (responseContentType.startsWith('text/html')) {
433
+ data = await response.text().then(result =>
434
+ // console.log('xml ********', result);
435
+ new DOMParser().parseFromString(result, 'text/html'),
436
+ );
437
+ } else {
438
+ data = 'done';
439
+ }
440
+ // const theFore = fxEvaluateXPathToFirstNode('//fx-fore', data.firstElementChild);
441
+ const theFore = data.querySelector('fx-fore');
442
+ const imported = document.importNode(theFore, true);
443
+
444
+ imported.classList.add('widget'); // is the new widget
445
+ imported.addEventListener(
446
+ 'model-construct-done',
447
+ e => {
448
+ const defaultInst = imported.querySelector('fx-instance');
449
+ if (this.initialNode) {
450
+ const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
451
+ // Note: Clone the input to prevent the inner fore from editing the outer node
452
+ doc.firstElementChild.appendChild(this.initialNode.cloneNode(true));
453
+ defaultInst.setInstanceData(doc);
454
+ }
455
+ imported.getModel().updateModel();
456
+ imported.refresh();
457
+ },
458
+ {once: true},
459
+ );
460
+
461
+ const dummy = this.querySelector('input');
462
+ if (this.hasAttribute('shadow')) {
463
+ dummy.parentNode.removeChild(dummy);
464
+ this.shadowRoot.appendChild(imported);
465
+ } else if (!this.loaded) {
466
+ dummy.replaceWith(imported);
467
+ }
297
468
 
298
- if (this.initial) {
299
- this.initialNode = evaluateXPathToFirstNode(this.initial, this.nodeset, this);
300
- console.log('initialNodes', this.initialNode);
301
- }
302
469
 
303
- // ### load the markup from Url
304
- await this._loadForeFromUrl();
305
- this.loaded = true;
470
+ if (!theFore) {
471
+ this.dispatchEvent(
472
+ new CustomEvent('error', {
473
+ detail: {
474
+ message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
475
+ },
476
+ }),
477
+ );
478
+ }
479
+ this.dispatchEvent(new CustomEvent('loaded', {detail: {fore: theFore}}));
480
+ } catch (error) {
481
+ // console.log('error', error);
482
+ this.getOwnerForm().dispatchEvent(
483
+ new CustomEvent('error', {detail: {message: `${this.url} not found`}}),
484
+ );
485
+ }
486
+ }
306
487
 
307
- // ### replace default instance of embedded Fore with initial nodes
308
- // const innerInstance = this.querySelector('fx-instance');
309
- // console.log('innerInstance',innerInstance);
310
- return;
488
+ getTemplate() {
489
+ return this.querySelector('template');
311
490
  }
312
491
 
313
- /*
314
- if(this.url && !this.loaded){
315
- this._loadForeFromUrl();
316
- this.loaded=true;
317
- return;
492
+ async refresh(force) {
493
+ // console.log('fx-control refresh', this);
494
+ super.refresh(force);
495
+ // console.log('refresh template', this.template);
496
+ // const {widget} = this;
497
+
498
+ // ### if we find a ref on control we have a 'select' control of some kind
499
+ const widget = this.getWidget();
500
+ this._handleBoundWidget(widget);
501
+ this._handleDataAttributeBinding();
502
+ Fore.refreshChildren(this, force);
318
503
  }
319
- */
320
- if (widget.value !== this.value) {
321
- widget.value = this.value;
504
+
505
+
506
+ /**
507
+ * handle non-Fore elements like 'select' and 'datalist' which have a 'data-ref' attribute
508
+ * @private
509
+ */
510
+ _handleDataAttributeBinding() {
511
+ const dataRefd = this.querySelector('[data-ref]');
512
+ if (dataRefd) {
513
+ this.boundList = dataRefd;
514
+ const ref = dataRefd.getAttribute('data-ref');
515
+ this._handleBoundWidget(dataRefd);
516
+ }
322
517
  }
323
- }
324
-
325
- /**
326
- * loads an external Fore from an HTML file given by `url` attribute.
327
- *
328
- * Will look for the `<fx-fore>` element within the returned HTML file and return that element.
329
- *
330
- * If that cannot be found an error is dispatched.
331
- *
332
- * todo: dispatch link error
333
- * @private
334
- */
335
- async _loadForeFromUrl() {
336
- console.log('########## loading Fore from ', this.url, '##########');
337
- console.info(
338
- `%cFore is processing URL ${this.url}`,
339
- "background:#64b5f6; color:white; padding:1rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;",
340
- );
341
- try {
342
- const response = await fetch(this.url, {
343
- method: 'GET',
344
- mode: 'cors',
345
- credentials: 'include',
346
- headers: {
347
- 'Content-Type': 'text/html',
348
- },
349
- });
350
-
351
- const responseContentType = response.headers.get('content-type').toLowerCase();
352
- console.log('********** responseContentType *********', responseContentType);
353
- let data;
354
- if (responseContentType.startsWith('text/html')) {
355
- data = await response.text().then(result =>
356
- // console.log('xml ********', result);
357
- new DOMParser().parseFromString(result, 'text/html'),
358
- );
359
- } else {
360
- data = 'done';
361
- }
362
- // const theFore = fxEvaluateXPathToFirstNode('//fx-fore', data.firstElementChild);
363
- const theFore = data.querySelector('fx-fore');
364
- const imported = document.importNode(theFore,true);
365
-
366
- // console.log('thefore', theFore)
367
- imported.classList.add('widget'); // is the new widget
368
- console.log(`########## loaded fore as component ##### ${this.url}`);
369
- imported.addEventListener(
370
- 'model-construct-done',
371
- e => {
372
- console.log('subcomponent ready', e.target);
373
- const defaultInst = imported.querySelector('fx-instance');
374
- // console.log('defaultInst', defaultInst);
375
- if(this.initialNode){
376
- const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
377
- // Note: Clone the input to prevent the inner fore from editing the outer node
378
- doc.firstElementChild.appendChild(this.initialNode.cloneNode(true));
379
- // defaultinst.setInstanceData(this.initialNode);
380
- defaultInst.setInstanceData(doc);
518
+
519
+ /**
520
+ * If the widget itself has a `ref` it binds to another nodeset to provide some
521
+ * dynamic items to be created from a template usually. Examples are dynamic select option lists
522
+ * or a set of checkboxes.
523
+ *
524
+ * @param widget the widget to handle
525
+ * @private
526
+ */
527
+ _handleBoundWidget(widget) {
528
+ if (this.boundInitialized && this.static) return;
529
+
530
+ const ref = widget.hasAttribute('ref') ? widget.getAttribute('ref') : widget.getAttribute('data-ref');
531
+ // if (widget && widget.hasAttribute('ref')) {
532
+ if (widget && ref) {
533
+ // ### eval nodeset for list control
534
+ const inscope = getInScopeContext(this, ref);
535
+ // const nodeset = evaluateXPathToNodes(ref, inscope, this);
536
+ const nodeset = evaluateXPath(ref, inscope, this);
537
+
538
+ // ### bail out when nodeset is array and empty
539
+ if (Array.isArray(nodeset) && nodeset.length === 0) return;
540
+
541
+ // ### clear items
542
+ const {children} = widget;
543
+ Array.from(children).forEach(child => {
544
+ if (child.nodeName.toLowerCase() !== 'template') {
545
+ child.parentNode.removeChild(child);
546
+ }
547
+ });
548
+
549
+ // ### build the items
550
+ const {template} = this;
551
+ if (template) {
552
+ // ### handle 'selection' open and insert an empty option in that case
553
+ if (this.widget.nodeName === "SELECT" &&
554
+ this.widget.hasAttribute('selection') &&
555
+ this.widget.getAttribute('selection') === 'open') {
556
+ const firstTemplateChild = this.template.firstElementChild;
557
+ // todo: create the element which is used in the template instead of 'option'
558
+ const option = document.createElement('option');
559
+ this.widget.insertBefore(option, firstTemplateChild);
560
+ }
561
+
562
+ if (nodeset.length) {
563
+ // console.log('nodeset', nodeset);
564
+ const fragment = document.createDocumentFragment();
565
+ // console.time('offscreen');
566
+ Array.from(nodeset).forEach(node => {
567
+ // console.log('#### node', node);
568
+ // ### initialize new entry
569
+ const newEntry = this.createEntry();
570
+ fragment.appendChild(newEntry);
571
+ // ### set value
572
+ this.updateEntry(newEntry, node);
573
+ });
574
+ this.template.parentNode.appendChild(fragment);
575
+ // console.timeEnd('offscreen');
576
+ } else {
577
+ const newEntry = this.createEntry();
578
+ this.template.parentNode.appendChild(newEntry);
579
+ this.updateEntry(newEntry, nodeset);
580
+ }
581
+ this.boundInitialized = true;
381
582
  }
382
- // console.log('new data', defaultInst.getInstanceData());
383
- // theFore.getModel().modelConstruct();
384
- imported.getModel().updateModel();
385
- imported.refresh();
386
- },
387
- { once: true },
388
- );
389
-
390
- const dummy = this.querySelector('input');
391
- if (this.hasAttribute('shadow')) {
392
- dummy.parentNode.removeChild(dummy);
393
- this.shadowRoot.appendChild(imported);
394
- } else {
395
- console.log(this, 'replacing widget with',theFore);
396
- dummy.replaceWith(imported);
397
- // this.appendChild(imported);
398
- }
399
-
400
-
401
- if (!theFore) {
402
- this.dispatchEvent(
403
- new CustomEvent('error', {
404
- detail: {
405
- message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
406
- },
407
- }),
408
- );
409
- }
410
- console.log('loaded');
411
- this.dispatchEvent(new CustomEvent('loaded', { detail: { fore: theFore } }));
412
- } catch (error) {
413
- console.log('error', error);
414
- this.getOwnerForm().dispatchEvent(
415
- new CustomEvent('error', { detail: { message: `${this.url} not found` } }),
416
- );
417
- }
418
- }
419
-
420
- getTemplate() {
421
- return this.querySelector('template');
422
- }
423
-
424
- async refresh(force) {
425
- // console.log('fx-control refresh', this);
426
- super.refresh(force);
427
- // console.log('refresh template', this.template);
428
- // const {widget} = this;
429
-
430
- // ### if we find a ref on control we have a 'select' control of some kind
431
- const widget = this.getWidget();
432
- this._handleBoundWidget(widget);
433
- Fore.refreshChildren(this, force);
434
- }
435
-
436
- /**
437
- * If the widget itself has a `ref` it binds to another nodeset to provide some
438
- * dynamic items to be created from a template usually. Examples are dynamic select option lists
439
- * or a set of checkboxes.
440
- *
441
- * @param widget the widget to handle
442
- * @private
443
- */
444
- _handleBoundWidget(widget) {
445
- if(this.boundInitialized && this.static) return;
446
-
447
- if (widget && widget.hasAttribute('ref')) {
448
- // ### eval nodeset for list control
449
- const ref = widget.getAttribute('ref');
450
- /*
451
- actually a ref on a select or similar component should point to a different instance
452
- with an absolute expr e.g. 'instance('theId')/...'
453
-
454
- todo: even bail out if ref is not absolute?
455
- */
456
-
457
- const inscope = getInScopeContext(this, ref);
458
- // const nodeset = evaluateXPathToNodes(ref, inscope, this);
459
- const nodeset = evaluateXPath(ref, inscope, this);
460
-
461
- // ### bail out when nodeset is array and empty
462
- if (Array.isArray(nodeset) && nodeset.length === 0) return;
463
-
464
- // ### clear items
465
- const { children } = widget;
466
- Array.from(children).forEach(child => {
467
- if (child.nodeName.toLowerCase() !== 'template') {
468
- child.parentNode.removeChild(child);
469
583
  }
470
- });
471
-
472
- // ### build the items
473
- if (this.template) {
474
- if(this.widget.nodeName === "SELECT" &&
475
- this.widget.hasAttribute('selection') &&
476
- this.widget.getAttribute('selection') === 'open'){
477
- const firstTemplateChild=this.template.firstElementChild;
478
- const option = document.createElement('option');
479
- this.widget.insertBefore(option,firstTemplateChild);
584
+ }
585
+
586
+ updateEntry(newEntry, node) {
587
+ // ### >>> todo: needs rework this code is heavily assuming a select control with 'value' attribute - not generic at all yet.
588
+
589
+ // if (this.widget.nodeName !== 'SELECT') return;
590
+ const valueAttribute = this._getValueAttribute(newEntry);
591
+ if (!valueAttribute) {
592
+ // Fore.dispatch(this,'warn',{message:'no value attribute specified for template entry.'});
593
+ return;
480
594
  }
481
595
 
482
- if (nodeset.length) {
483
- // console.log('nodeset', nodeset);
484
- const fragment = document.createDocumentFragment();
485
- // console.time('offscreen');
486
- /*
487
- Array.from(nodeset).forEach(node => {
488
- // console.log('#### node', node);
489
- const newEntry = this.createEntry();
490
- this.template.parentNode.appendChild(newEntry);
491
- // ### initialize new entry
492
- // ### set value
493
- this.updateEntry(newEntry, node);
494
- });
495
- */
496
- // this should actually perform better than the above but does not seem to make a measurable difference.
497
- Array.from(nodeset).forEach(node => {
498
- // console.log('#### node', node);
499
- const newEntry = this.createEntry();
500
- fragment.appendChild(newEntry);
501
-
502
- // ### initialize new entry
503
- // ### set value
504
- this.updateEntry(newEntry, node);
505
- });
506
- this.template.parentNode.appendChild(fragment);
507
- // console.timeEnd('offscreen');
508
- } else {
509
- const newEntry = this.createEntry();
510
- this.template.parentNode.appendChild(newEntry);
511
- this.updateEntry(newEntry, nodeset);
596
+ const valueExpr = valueAttribute.value;
597
+ const cutted = valueExpr.substring(1, valueExpr.length - 1);
598
+ const evaluated = evaluateXPathToString(cutted, node, newEntry);
599
+ valueAttribute.value = evaluated;
600
+
601
+ if (this.value === evaluated) {
602
+ newEntry.setAttribute('selected', 'selected');
512
603
  }
513
- this.boundInitialized = true;
514
- }
604
+
605
+ // ### set label
606
+ const optionLabel = newEntry.textContent;
607
+ this.evalLabel(optionLabel, node, newEntry);
608
+ // ### <<< needs rework
515
609
  }
516
- }
517
610
 
518
- updateEntry(newEntry, node) {
519
- // ### >>> todo: needs rework this code is heavily assuming a select control with 'value' attribute - not generic at all yet.
520
611
 
521
- if (this.widget.nodeName !== 'SELECT') return;
522
- const valueAttribute = this._getValueAttribute(newEntry);
523
- const valueExpr = valueAttribute.value;
524
- const cutted = valueExpr.substring(1, valueExpr.length - 1);
525
- const evaluated = evaluateXPathToString(cutted, node, newEntry);
526
- valueAttribute.value = evaluated;
612
+ evalLabel(optionLabel, node, newEntry) {
613
+ const labelExpr = optionLabel.substring(1, optionLabel.length - 1);
614
+ if (!labelExpr) return;
615
+
616
+ const label = evaluateXPathToString(labelExpr, node, this);
617
+ newEntry.textContent = label;
618
+ }
527
619
 
528
- if (this.value === evaluated) {
529
- newEntry.setAttribute('selected', 'selected');
620
+ createEntry() {
621
+ return this.template.content.firstElementChild.cloneNode(true);
622
+ // const content = this.template.content.firstElementChild.cloneNode(true);
623
+ // return content;
624
+ // const newEntry = document.importNode(content, true);
625
+ // this.template.parentNode.appendChild(newEntry);
626
+ // return newEntry;
530
627
  }
531
628
 
532
- // ### set label
533
- const optionLabel = newEntry.textContent;
534
- const labelExpr = optionLabel.substring(1, optionLabel.length - 1);
535
-
536
- const label = evaluateXPathToString(labelExpr, node, newEntry);
537
- newEntry.textContent = label;
538
- // ### <<< needs rework
539
- }
540
-
541
- createEntry() {
542
- return this.template.content.firstElementChild.cloneNode(true);
543
- // const content = this.template.content.firstElementChild.cloneNode(true);
544
- // return content;
545
- // const newEntry = document.importNode(content, true);
546
- // this.template.parentNode.appendChild(newEntry);
547
- // return newEntry;
548
- }
549
-
550
- // eslint-disable-next-line class-methods-use-this
551
- _getValueAttribute(element) {
552
- let result;
553
- Array.from(element.attributes).forEach(attribute => {
554
- const attrVal = attribute.value;
555
- if (attrVal.indexOf('{') !== -1) {
556
- // console.log('avt found ', attribute);
557
- result = attribute;
558
- }
559
- });
560
- return result;
561
- }
629
+ // eslint-disable-next-line class-methods-use-this
630
+ _getValueAttribute(element) {
631
+ let result;
632
+ Array.from(element.attributes).forEach(attribute => {
633
+ const attrVal = attribute.value;
634
+ if (attrVal.indexOf('{') !== -1) {
635
+ // console.log('avt found ', attribute);
636
+ result = attribute;
637
+ }
638
+ });
639
+ return result;
640
+ }
562
641
  }
563
642
  if (!customElements.get('fx-control')) {
564
- window.customElements.define('fx-control', FxControl);
643
+ window.customElements.define('fx-control', FxControl);
565
644
  }