@jinntec/fore 2.2.0 → 2.3.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 (88) hide show
  1. package/README.md +1 -1
  2. package/dist/fore-dev.js +2 -2
  3. package/dist/fore-dev.js.map +1 -1
  4. package/dist/fore.js +2 -2
  5. package/dist/fore.js.map +1 -1
  6. package/index.js +3 -2
  7. package/package.json +10 -38
  8. package/resources/fore.css +263 -262
  9. package/resources/vars.css +8 -0
  10. package/src/DependencyNotifyingDomFacade.js +1 -0
  11. package/src/ForeElementMixin.js +243 -228
  12. package/src/actions/abstract-action.js +407 -398
  13. package/src/actions/fx-action.js +6 -6
  14. package/src/actions/fx-append.js +1 -1
  15. package/src/actions/fx-call.js +63 -62
  16. package/src/actions/fx-confirm.js +11 -11
  17. package/src/actions/fx-construct-done.js +4 -4
  18. package/src/actions/fx-copy.js +36 -36
  19. package/src/actions/fx-delete.js +77 -79
  20. package/src/actions/fx-dispatch.js +14 -14
  21. package/src/actions/fx-hide.js +15 -15
  22. package/src/actions/fx-insert.js +9 -12
  23. package/src/actions/fx-insertchild.js +88 -0
  24. package/src/actions/fx-load.js +188 -185
  25. package/src/actions/fx-message.js +18 -19
  26. package/src/actions/fx-refresh.js +10 -10
  27. package/src/actions/fx-reload.js +16 -14
  28. package/src/actions/fx-replace.js +0 -1
  29. package/src/actions/fx-reset.js +31 -31
  30. package/src/actions/fx-send.js +59 -52
  31. package/src/actions/fx-setattribute.js +48 -49
  32. package/src/actions/fx-setfocus.js +54 -58
  33. package/src/actions/fx-setvalue.js +85 -81
  34. package/src/actions/fx-show.js +11 -11
  35. package/src/actions/fx-toggle.js +2 -2
  36. package/src/actions/fx-toggleboolean.js +38 -39
  37. package/src/actions/fx-unmodified.js +27 -0
  38. package/src/actions/fx-update.js +6 -6
  39. package/src/dep_graph.js +1 -1
  40. package/src/drawdown.js +23 -30
  41. package/src/events.js +19 -19
  42. package/src/fore.js +143 -125
  43. package/src/functions/common-function.js +24 -25
  44. package/src/functions/fx-function.js +4 -6
  45. package/src/functions/fx-functionlib.js +39 -42
  46. package/src/functions/registerFunction.js +95 -86
  47. package/src/fx-bind.js +19 -24
  48. package/src/fx-connection.js +226 -0
  49. package/src/fx-fore.js +846 -815
  50. package/src/fx-header.js +4 -4
  51. package/src/fx-instance.js +23 -27
  52. package/src/fx-model.js +405 -383
  53. package/src/fx-submission.js +399 -397
  54. package/src/fx-var.js +6 -5
  55. package/src/getInScopeContext.js +102 -89
  56. package/src/json-util.js +24 -24
  57. package/src/lab/fore-component.js +48 -52
  58. package/src/lab/instance-inspector.js +13 -16
  59. package/src/modelitem.js +48 -10
  60. package/src/relevance.js +12 -16
  61. package/src/tools/adi.js +858 -812
  62. package/src/tools/fx-action-log.js +377 -295
  63. package/src/tools/fx-devtools.js +210 -210
  64. package/src/tools/fx-dom-inspector.js +123 -122
  65. package/src/tools/fx-json-instance.js +262 -253
  66. package/src/tools/fx-log-item.js +6 -11
  67. package/src/tools/fx-log-settings.js +358 -301
  68. package/src/tools/fx-minimap.js +186 -177
  69. package/src/tools/helpers.js +1 -1
  70. package/src/ui/abstract-control.js +74 -56
  71. package/src/ui/fx-case.js +59 -7
  72. package/src/ui/fx-container.js +13 -14
  73. package/src/ui/fx-control.js +572 -538
  74. package/src/ui/fx-dialog.js +3 -3
  75. package/src/ui/fx-droptarget.js +3 -4
  76. package/src/ui/fx-group.js +4 -2
  77. package/src/ui/fx-hint.js +3 -0
  78. package/src/ui/fx-inspector.js +5 -5
  79. package/src/ui/fx-items.js +11 -11
  80. package/src/ui/fx-output.js +10 -12
  81. package/src/ui/fx-repeat-attributes.js +23 -27
  82. package/src/ui/fx-repeat.js +347 -344
  83. package/src/ui/fx-repeatitem.js +75 -68
  84. package/src/ui/fx-switch.js +39 -14
  85. package/src/ui/fx-trigger.js +4 -4
  86. package/src/withDraggability.js +193 -191
  87. package/src/xpath-evaluation.js +969 -959
  88. package/src/xpath-util.js +161 -134
package/src/fx-fore.js CHANGED
@@ -1,11 +1,29 @@
1
- import {Fore} from './fore.js';
1
+ import { Fore } from './fore.js';
2
2
  import './fx-instance.js';
3
- import {FxModel} from './fx-model.js';
3
+ import { FxModel } from './fx-model.js';
4
4
  import '@jinntec/jinn-toast';
5
- import {evaluateXPathToBoolean, evaluateXPathToNodes, evaluateXPathToFirstNode, evaluateXPathToString} from './xpath-evaluation.js';
5
+ import {
6
+ evaluateXPathToBoolean,
7
+ evaluateXPathToNodes,
8
+ evaluateXPathToFirstNode,
9
+ evaluateXPathToString,
10
+ } from './xpath-evaluation.js';
6
11
  import getInScopeContext from './getInScopeContext.js';
7
- import {XPathUtil} from './xpath-util.js';
8
- import {FxRepeatAttributes} from './ui/fx-repeat-attributes.js';
12
+ import { XPathUtil } from './xpath-util.js';
13
+ import { FxRepeatAttributes } from './ui/fx-repeat-attributes.js';
14
+
15
+ /**
16
+ * Makes the dirty state of the form.
17
+ *
18
+ * Clean when there are no changes yet or all is submitted,
19
+ * Dirty when there are unsaved changes.
20
+ *
21
+ * We might need a 'saving' state later
22
+ */
23
+ const dirtyStates = {
24
+ CLEAN: 'clean',
25
+ DIRTY: 'dirty',
26
+ };
9
27
 
10
28
  /**
11
29
  * Main class for Fore.Outermost container element for each Fore application.
@@ -25,88 +43,92 @@ import {FxRepeatAttributes} from './ui/fx-repeat-attributes.js';
25
43
  * @ts-check
26
44
  */
27
45
  export class FxFore extends HTMLElement {
28
- static outermostHandler = null;
29
- static draggedItem = null;
30
-
31
- static get properties() {
32
- return {
33
- /**
34
- * ignore certain nodes for template expression search
35
- */
36
- ignoreExpressions:{
37
- type: String
38
- },
39
- /**
40
- * merge-partial
41
- */
42
- mergePartial:{
43
- type: Boolean
44
- },
45
- /**
46
- * Setting this marker attribute will refresh the UI in a lazy fashion just updating elements being
47
- * in viewport.
48
- *
49
- * this feature is still experimental and should be used with caution and extra testing
50
- */
51
- lazyRefresh: {
52
- type: Boolean,
53
- },
54
- model: {
55
- type: Object,
56
- },
57
- ready: {
58
- type: Boolean,
59
- },
60
- strict:{
61
- type: Boolean
62
- },
63
- /**
64
- *
65
- */
66
- validateOn: {
67
- type: String
68
- },
69
- version: {
70
- type: String
71
- }
72
- };
73
- }
74
-
75
- /**
76
- * attaches handlers for
77
- *
78
- * - `model-construct-done` to trigger the processing of the UI
79
- * - `message` - to display a message triggered by an fx-message action
80
- * - `error` - to display an error message
81
- * - 'compute-exception` - warn about circular dependencies in graph
82
- */
83
- constructor() {
84
- super();
85
- this.version = '[VI]Version: {version} - built on {date}[/VI]';
86
-
87
- this.model = {};
88
- this.inited=false;
89
- // this.addEventListener('model-construct-done', this._handleModelConstructDone);
90
- // todo: refactoring - these should rather go into connectedcallback
91
- this.addEventListener('message', this._displayMessage);
92
- // this.addEventListener('error', this._displayError);
93
- this.addEventListener('error', this._logError);
94
- this.addEventListener('warn', this._displayWarning);
95
- // this.addEventListener('log', this._logError);
96
- window.addEventListener('compute-exception', e => {
97
- console.error('circular dependency: ', e);
98
- });
99
-
100
- this.ready = false;
101
- this.storedTemplateExpressionByNode = new Map();
102
-
103
- // Stores the outer most action handler. If an action handler is already running, all
104
- // updates are included in that one
105
- this.outermostHandler = null;
106
-
107
- this.copiedElements = new WeakSet();
108
-
109
- const style = `
46
+ static outermostHandler = null;
47
+
48
+ static draggedItem = null;
49
+
50
+ static get properties() {
51
+ return {
52
+ /**
53
+ * ignore certain nodes for template expression search
54
+ */
55
+ ignoreExpressions: {
56
+ type: String,
57
+ },
58
+ /**
59
+ * merge-partial
60
+ */
61
+ mergePartial: {
62
+ type: Boolean,
63
+ },
64
+ /**
65
+ * Setting this marker attribute will refresh the UI in a lazy fashion just updating elements being
66
+ * in viewport.
67
+ *
68
+ * this feature is still experimental and should be used with caution and extra testing
69
+ */
70
+ lazyRefresh: {
71
+ type: Boolean,
72
+ },
73
+ model: {
74
+ type: Object,
75
+ },
76
+ ready: {
77
+ type: Boolean,
78
+ },
79
+ strict: {
80
+ type: Boolean,
81
+ },
82
+ /**
83
+ *
84
+ */
85
+ validateOn: {
86
+ type: String,
87
+ },
88
+ version: {
89
+ type: String,
90
+ },
91
+ };
92
+ }
93
+
94
+ /**
95
+ * attaches handlers for
96
+ *
97
+ * - `model-construct-done` to trigger the processing of the UI
98
+ * - `message` - to display a message triggered by an fx-message action
99
+ * - `error` - to display an error message
100
+ * - 'compute-exception` - warn about circular dependencies in graph
101
+ */
102
+ constructor() {
103
+ super();
104
+ this.version = '[VI]Version: {version} - built on {date}[/VI]';
105
+
106
+ this.model = {};
107
+ this.inited = false;
108
+ // this.addEventListener('model-construct-done', this._handleModelConstructDone);
109
+ // todo: refactoring - these should rather go into connectedcallback
110
+ this.addEventListener('message', this._displayMessage);
111
+ // this.addEventListener('error', this._displayError);
112
+ this.addEventListener('error', this._logError);
113
+ this.addEventListener('warn', this._displayWarning);
114
+ // this.addEventListener('log', this._logError);
115
+ window.addEventListener('compute-exception', e => {
116
+ console.error('circular dependency: ', e);
117
+ });
118
+
119
+ this.ready = false;
120
+ this.storedTemplateExpressionByNode = new Map();
121
+
122
+ // Stores the outer most action handler. If an action handler is already running, all
123
+ // updates are included in that one
124
+ this.outermostHandler = null;
125
+
126
+ this.copiedElements = new WeakSet();
127
+
128
+ this.dirtyState = dirtyStates.CLEAN;
129
+ this.showConfirmation = false;
130
+
131
+ const style = `
110
132
  :host {
111
133
  display: block;
112
134
  }
@@ -181,7 +203,7 @@ export class FxFore extends HTMLElement {
181
203
  }
182
204
  `;
183
205
 
184
- const html = `
206
+ const html = `
185
207
  <!-- <slot name="errors"></slot> -->
186
208
  <jinn-toast id="message" gravity="bottom" position="left"></jinn-toast>
187
209
  <jinn-toast id="sticky" gravity="bottom" position="left" duration="-1" close="true" data-class="sticky-message"></jinn-toast>
@@ -199,28 +221,30 @@ export class FxFore extends HTMLElement {
199
221
  <slot name="event"></slot>
200
222
  `;
201
223
 
202
- this.attachShadow({mode: 'open'});
203
- this.shadowRoot.innerHTML = `
224
+ this.attachShadow({ mode: 'open' });
225
+ this.shadowRoot.innerHTML = `
204
226
  <style>
205
227
  ${style}
206
228
  </style>
207
229
  ${html}
208
230
  `;
209
231
 
210
- this.toRefresh = [];
211
- this.initialRun = true;
212
- this._scanForNewTemplateExpressionsNextRefresh = false;
213
- this.repeatsFromAttributesCreated = false;
214
- this.validateOn = this.hasAttribute('validate-on') ? this.getAttribute('validate-on'):'update';
215
- // this.mergePartial = this.hasAttribute('merge-partial')? true:false;
216
- this.mergePartial = false;
217
- }
218
-
219
- connectedCallback() {
220
- this.style.visibility = 'hidden';
221
- console.time('init');
222
- this.strict = this.hasAttribute('strict') ? true : false;
223
- /*
232
+ this.toRefresh = [];
233
+ this.initialRun = true;
234
+ this._scanForNewTemplateExpressionsNextRefresh = false;
235
+ this.repeatsFromAttributesCreated = false;
236
+ this.validateOn = this.hasAttribute('validate-on')
237
+ ? this.getAttribute('validate-on')
238
+ : 'update';
239
+ // this.mergePartial = this.hasAttribute('merge-partial')? true:false;
240
+ this.mergePartial = false;
241
+ }
242
+
243
+ connectedCallback() {
244
+ this.style.visibility = 'hidden';
245
+ console.time('init');
246
+ this.strict = !!this.hasAttribute('strict');
247
+ /*
224
248
  document.addEventListener('ready', (e) =>{
225
249
  if(e.target !== this){
226
250
  // e.preventDefault();
@@ -238,209 +262,230 @@ export class FxFore extends HTMLElement {
238
262
  // e.stopImmediatePropagation();
239
263
  },true);
240
264
  */
241
- this.ignoreExpressions = this.hasAttribute('ignore-expressions') ? this.getAttribute('ignore-expressions'): null;
242
-
243
- this.lazyRefresh = this.hasAttribute('refresh-on-view');
244
- if (this.lazyRefresh) {
245
- const options = {
246
- root: null,
247
- rootMargin: '0px',
248
- threshold: 0.3,
249
- };
250
- this.intersectionObserver = new IntersectionObserver(this.handleIntersect, options);
251
- }
252
-
253
- this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
254
- if (this.src) {
255
- this._loadFromSrc();
256
- return;
257
- }
258
-
259
- this._injectDevtools();
260
-
261
- const slot = this.shadowRoot.querySelector('slot#default');
262
- slot.addEventListener('slotchange', async event => {
263
- // preliminary addition for auto-conversion of non-prefixed element into prefixed elements. See fore.js
264
- console.log(`### <<<<< slotchange on '${this.id}' >>>>>`);
265
- if(this.inited) return;
266
- if(this.hasAttribute('convert')){
267
- this.replaceWith(Fore.copyDom(this));
268
- // Fore.copyDom(this);
269
- return;
270
- }
265
+ this.ignoreExpressions = this.hasAttribute('ignore-expressions')
266
+ ? this.getAttribute('ignore-expressions')
267
+ : null;
268
+
269
+ this.lazyRefresh = this.hasAttribute('refresh-on-view');
270
+ if (this.lazyRefresh) {
271
+ const options = {
272
+ root: null,
273
+ rootMargin: '0px',
274
+ threshold: 0.3,
275
+ };
276
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersect, options);
277
+ }
271
278
 
272
- if(this.ignoreExpressions){
273
- this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
274
- }
279
+ this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
280
+ if (this.src) {
281
+ this._loadFromSrc();
282
+ return;
283
+ }
275
284
 
276
- const children = event.target.assignedElements();
277
- let modelElement = children.find(
278
- modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
279
- );
280
- if (!modelElement) {
281
- const generatedModel = document.createElement('fx-model');
282
- this.appendChild(generatedModel);
283
- modelElement = generatedModel;
284
- // We are going to get a new slotchange event immediately, because we changed a slot.
285
- // so cancel this one.
286
- return;
287
- }
288
- if (!modelElement.inited) {
289
- console.info(
290
- `%cFore is processing fx-fore#${this.id}`,
291
- "background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;",
292
- );
293
-
294
- const variables = new Map();
295
- (function registerVariables(node) {
296
- for (const child of node.children) {
297
- if ('setInScopeVariables' in child) {
298
- child.setInScopeVariables(variables);
299
- }
300
- registerVariables(child);
301
- }
302
- })(this);
303
-
304
- await modelElement.modelConstruct();
305
- this._handleModelConstructDone();
285
+ this._injectDevtools();
286
+
287
+ const slot = this.shadowRoot.querySelector('slot#default');
288
+ slot.addEventListener('slotchange', async event => {
289
+ // preliminary addition for auto-conversion of non-prefixed element into prefixed elements. See fore.js
290
+ // console.log(`### <<<<< slotchange on '${this.id}' >>>>>`);
291
+ if (this.inited) return;
292
+ if (this.hasAttribute('convert')) {
293
+ this.replaceWith(Fore.copyDom(this));
294
+ // Fore.copyDom(this);
295
+ return;
296
+ }
297
+
298
+ if (this.ignoreExpressions) {
299
+ this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
300
+ }
301
+
302
+ const children = event.target.assignedElements();
303
+ let modelElement = children.find(
304
+ modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
305
+ );
306
+ if (!modelElement) {
307
+ const generatedModel = document.createElement('fx-model');
308
+ this.appendChild(generatedModel);
309
+ modelElement = generatedModel;
310
+ // We are going to get a new slotchange event immediately, because we changed a slot.
311
+ // so cancel this one.
312
+ return;
313
+ }
314
+ if (!modelElement.inited) {
315
+ console.info(
316
+ `%cFore is processing fx-fore#${this.id}`,
317
+ 'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
318
+ );
319
+
320
+ const variables = new Map();
321
+ (function registerVariables(node) {
322
+ for (const child of node.children) {
323
+ if ('setInScopeVariables' in child) {
324
+ child.setInScopeVariables(variables);
306
325
  }
307
- this.model = modelElement;
308
-
309
- this._createRepeatsFromAttributes();
310
- this.inited = true;
311
-
312
- });
313
- this.addEventListener('path-mutated', () => {
314
- this.someInstanceDataStructureChanged = true;
315
- });
316
- this.addEventListener('refresh', () => {
317
- this.refresh(true);
318
- });
319
-
326
+ registerVariables(child);
327
+ }
328
+ })(this);
329
+
330
+ await modelElement.modelConstruct();
331
+ this._handleModelConstructDone();
332
+ }
333
+ this.model = modelElement;
334
+
335
+ this._createRepeatsFromAttributes();
336
+ this.inited = true;
337
+ });
338
+ this.addEventListener('path-mutated', () => {
339
+ this.someInstanceDataStructureChanged = true;
340
+ });
341
+ this.addEventListener('refresh', () => {
342
+ this.refresh(true);
343
+ });
344
+
345
+ if (this.hasAttribute('show-confirmation')) {
346
+ this.showConfirmation = true;
320
347
  }
348
+ }
321
349
 
322
- _injectDevtools(){
323
- if (this.ownerDocument.querySelector('fx-devtools')) {
324
- // There's already a devtools, so we can ignore this one.
325
- // One devtools can focus multiple fore elements
326
- return;
327
- }
328
- const search = window.location.search;
329
- const urlParams = new URLSearchParams(search);
330
- if(urlParams.has('inspect')){
331
- const devtools = document.createElement('fx-devtools');
332
- document.body.appendChild(devtools);
333
- }
350
+ _injectDevtools() {
351
+ if (this.ownerDocument.querySelector('fx-devtools')) {
352
+ // There's already a devtools, so we can ignore this one.
353
+ // One devtools can focus multiple fore elements
354
+ return;
334
355
  }
335
- addToRefresh(modelItem) {
336
- const found = this.toRefresh.find(mi => mi.path === modelItem.path);
337
- if (!found) {
338
- this.toRefresh.push(modelItem);
339
- }
356
+ const { search } = window.location;
357
+ const urlParams = new URLSearchParams(search);
358
+ if (urlParams.has('inspect')) {
359
+ const devtools = document.createElement('fx-devtools');
360
+ document.body.appendChild(devtools);
340
361
  }
341
-
342
- /**
343
- * Raise a flag that there might be new template expressions under some node. This happens with
344
- * repeats updating (new repeat items can have new template expressions) or switches changing their case (new case = new raw HTML)
345
- */
346
- scanForNewTemplateExpressionsNextRefresh() {
347
- // TODO: also ask for the root of any new HTML: this can prevent some very deep queries.
348
- this._scanForNewTemplateExpressionsNextRefresh = true;
349
- }
350
-
351
- /**
352
- * loads a Fore from an URL given by `src`.
353
- *
354
- * Will extract the `fx-fore` element from that target file and use and replace current `fx-fore` element with the loaded one.
355
- * @private
356
- */
357
- async _loadFromSrc() {
358
- // console.log('########## loading Fore from ', this.src, '##########');
359
- await Fore.loadForeFromSrc(this,this.src,'fx-fore');
362
+ }
363
+
364
+ /**
365
+ * Add a model item to the refresh list
366
+ *
367
+ * @param {import('./modelitem.js').ModelItem} modelItem
368
+ * @returns {void}
369
+ */
370
+ addToRefresh(modelItem) {
371
+ const found = this.toRefresh.find(mi => mi.path === modelItem.path);
372
+ if (!found) {
373
+ this.toRefresh.push(modelItem);
360
374
  }
375
+ }
376
+
377
+ /**
378
+ * Raise a flag that there might be new template expressions under some node. This happens with
379
+ * repeats updating (new repeat items can have new template expressions) or switches changing their case (new case = new raw HTML)
380
+ */
381
+ scanForNewTemplateExpressionsNextRefresh() {
382
+ // TODO: also ask for the root of any new HTML: this can prevent some very deep queries.
383
+ this._scanForNewTemplateExpressionsNextRefresh = true;
384
+ }
385
+
386
+ markAsClean() {
387
+ this.addEventListener(
388
+ 'value-changed',
389
+ () => {
390
+ this.dirtyState = dirtyStates.DIRTY;
391
+ },
392
+ { once: true },
393
+ );
394
+ this.dirtyState = dirtyStates.CLEAN;
395
+ }
396
+
397
+ /**
398
+ * loads a Fore from an URL given by `src`.
399
+ *
400
+ * Will extract the `fx-fore` element from that target file and use and replace current `fx-fore` element with the loaded one.
401
+ * @private
402
+ */
403
+ async _loadFromSrc() {
404
+ // console.log('########## loading Fore from ', this.src, '##########');
405
+ await Fore.loadForeFromSrc(this, this.src, 'fx-fore');
406
+ }
407
+
408
+ /**
409
+ * refreshes the UI by using IntersectionObserver API. This is the handler being called
410
+ * by the observer whenever elements come into / move out of viewport.
411
+ * @param entries
412
+ * @param observer
413
+ */
414
+ handleIntersect(entries, observer) {
415
+ // console.time('refreshLazy');
416
+
417
+ entries.forEach(entry => {
418
+ const { target } = entry;
419
+
420
+ const fore = Fore.getFore(target);
421
+ if (fore.initialRun) return;
422
+
423
+ if (entry.isIntersecting) {
424
+ // console.log('in view', entry);
425
+ // console.log('repeat in view entry', entry.target);
426
+ // const target = entry.target;
427
+ // if(target.hasAttribute('refresh-on-view')){
428
+ target.classList.add('loaded');
429
+ // }
430
+
431
+ // todo: too restrictive here? what if target is a usual html element? shouldn't it refresh downwards?
432
+ if (typeof target.refresh === 'function') {
433
+ // console.log('refreshing target', target);
434
+ target.refresh(target, true);
435
+ } else {
436
+ // console.log('refreshing children', target);
437
+ Fore.refreshChildren(target, true);
438
+ }
439
+ }
440
+ });
441
+ entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
361
442
 
362
- /**
363
- * refreshes the UI by using IntersectionObserver API. This is the handler being called
364
- * by the observer whenever elements come into / move out of viewport.
365
- * @param entries
366
- * @param observer
367
- */
368
- handleIntersect(entries, observer) {
369
- // console.time('refreshLazy');
370
-
371
- entries.forEach(entry => {
372
- const {target} = entry;
373
-
374
- const fore = Fore.getFore(target);
375
- if(fore.initialRun) return;
376
-
377
- if (entry.isIntersecting) {
378
- // console.log('in view', entry);
379
- // console.log('repeat in view entry', entry.target);
380
- // const target = entry.target;
381
- // if(target.hasAttribute('refresh-on-view')){
382
- target.classList.add('loaded');
383
- // }
384
-
385
- // todo: too restrictive here? what if target is a usual html element? shouldn't it refresh downwards?
386
- if (typeof target.refresh === 'function') {
387
- // console.log('refreshing target', target);
388
- target.refresh(target, true);
389
- } else {
390
- // console.log('refreshing children', target);
391
- Fore.refreshChildren(target, true);
392
- }
393
- }
394
- });
395
- entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
396
-
397
- // console.timeEnd('refreshLazy');
398
- }
443
+ // console.timeEnd('refreshLazy');
444
+ }
399
445
 
400
- evaluateToNodes(xpath, context) {
401
- return evaluateXPathToNodes(xpath, context, this);
402
- }
446
+ evaluateToNodes(xpath, context) {
447
+ return evaluateXPathToNodes(xpath, context, this);
448
+ }
403
449
 
404
- disconnectedCallback() {
405
- this.removeEventListener('dragstart', this.dragstart);
406
- /*
450
+ disconnectedCallback() {
451
+ this.removeEventListener('dragstart', this.dragstart);
452
+ /*
407
453
  this.removeEventListener('model-construct-done', this._handleModelConstructDone);
408
454
  this.removeEventListener('message', this._displayMessage);
409
455
  this.removeEventListener('error', this._displayError);
410
456
  this.storedTemplateExpressionByNode=null;
411
457
  this.shadowRoot = undefined;
412
458
  */
413
- }
414
-
415
- /**
416
- * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
417
- * the bound modelItem to the bound element.
418
- *
419
- *
420
- * force - boolean - if true will refresh all children disregarding toRefresh array
421
- *
422
- */
423
- async forceRefresh() {
424
- // console.time('refresh');
425
- // console.group('### forced refresh', this);
426
-
427
- Fore.refreshChildren(this, true);
428
- this._updateTemplateExpressions();
429
- this._scanForNewTemplateExpressionsNextRefresh = false; // reset
430
- this._processTemplateExpressions();
431
-
432
- console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
433
-
434
- Fore.dispatch(this, 'refresh-done', {});
435
-
436
- // console.groupEnd();
437
- // console.timeEnd('refresh');
438
- }
439
-
440
- // async refresh(force, changedPaths) {
441
- async refresh(force) {
442
-
443
- /*
459
+ }
460
+
461
+ /**
462
+ * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
463
+ * the bound modelItem to the bound element.
464
+ *
465
+ *
466
+ * force - boolean - if true will refresh all children disregarding toRefresh array
467
+ *
468
+ */
469
+ async forceRefresh() {
470
+ // console.time('refresh');
471
+ // console.group('### forced refresh', this);
472
+
473
+ Fore.refreshChildren(this, true);
474
+ this._updateTemplateExpressions();
475
+ this._scanForNewTemplateExpressionsNextRefresh = false; // reset
476
+ this._processTemplateExpressions();
477
+
478
+ // console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
479
+
480
+ Fore.dispatch(this, 'refresh-done', {});
481
+
482
+ // console.groupEnd();
483
+ // console.timeEnd('refresh');
484
+ }
485
+
486
+ // async refresh(force, changedPaths) {
487
+ async refresh(force) {
488
+ /*
444
489
 
445
490
  if (!changedPaths) {
446
491
  changedPaths = this.toRefresh.map(item => item.path);
@@ -475,89 +520,89 @@ export class FxFore extends HTMLElement {
475
520
  }
476
521
  }
477
522
  */
478
- if (this.isRefreshing) {
479
- return;
480
- }
481
- this.isRefreshing = true;
482
- console.log(`### <<<<< refresh() on '${this.id}' >>>>>`);
483
-
484
- // refresh () {
485
- // ### refresh Fore UI elements
486
- // if (!this.initialRun && this.toRefresh.length !== 0) {
487
- if (!force && !this.initialRun && this.toRefresh.length !== 0) {
488
- // console.log('toRefresh', this.toRefresh);
489
- let needsRefresh = false;
490
-
491
- // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
492
- this.toRefresh.forEach(modelItem => {
493
- // check if modelItem has boundControls - if so, call refresh() for each of them
494
- const controlsToRefresh = modelItem.boundControls;
495
- if (controlsToRefresh) {
496
- controlsToRefresh.forEach(ctrl => {
497
- ctrl.refresh(force);
498
- });
499
- }
523
+ if (this.isRefreshing) {
524
+ return;
525
+ }
526
+ this.isRefreshing = true;
527
+ // console.log(`### <<<<< refresh() on '${this.id}' >>>>>`);
528
+
529
+ // refresh () {
530
+ // ### refresh Fore UI elements
531
+ // if (!this.initialRun && this.toRefresh.length !== 0) {
532
+ if (!force && !this.initialRun && this.toRefresh.length !== 0) {
533
+ // console.log('toRefresh', this.toRefresh);
534
+ let needsRefresh = false;
535
+
536
+ // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
537
+ this.toRefresh.forEach(modelItem => {
538
+ // check if modelItem has boundControls - if so, call refresh() for each of them
539
+ const controlsToRefresh = modelItem.boundControls;
540
+ if (controlsToRefresh) {
541
+ controlsToRefresh.forEach(ctrl => {
542
+ ctrl.refresh(force);
543
+ });
544
+ }
500
545
 
501
- // ### check if other controls depend on current modelItem
502
- const { mainGraph } = this.getModel();
503
- if (mainGraph && mainGraph.hasNode(modelItem.path)) {
504
- const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
505
- // ### iterate dependant modelItems and refresh all their boundControls
506
- if (deps.length !== 0) {
507
- deps.forEach(dep => {
508
- // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
509
- const basePath = XPathUtil.getBasePath(dep);
510
- const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
511
- // ### refresh all boundControls
512
- modelItemOfDep.boundControls.forEach(control => {
513
- control.refresh(force);
514
- });
515
- });
516
- needsRefresh = true;
517
- }
518
- }
546
+ // ### check if other controls depend on current modelItem
547
+ const { mainGraph } = this.getModel();
548
+ if (mainGraph && mainGraph.hasNode(modelItem.path)) {
549
+ const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
550
+ // ### iterate dependant modelItems and refresh all their boundControls
551
+ if (deps.length !== 0) {
552
+ deps.forEach(dep => {
553
+ // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
554
+ const basePath = XPathUtil.getBasePath(dep);
555
+ const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
556
+ // ### refresh all boundControls
557
+ modelItemOfDep.boundControls.forEach(control => {
558
+ control.refresh(force);
559
+ });
519
560
  });
520
- this.toRefresh = [];
521
- /*
561
+ needsRefresh = true;
562
+ }
563
+ }
564
+ });
565
+ this.toRefresh = [];
566
+ /*
522
567
  if (!needsRefresh) {
523
568
  console.log('no dependants to refresh');
524
569
  }
525
570
  */
526
- } else {
527
- // ### resetting visited state for controls to refresh
528
- /*
571
+ } else {
572
+ // ### resetting visited state for controls to refresh
573
+ /*
529
574
  const visited = this.parentNode.querySelectorAll('.visited');
530
575
  Array.from(visited).forEach(v =>{
531
576
  v.classList.remove('visited');
532
577
  });
533
578
  */
534
579
 
535
- if(this.inited){
536
- Fore.refreshChildren(this, true);
537
- }
538
- // console.timeEnd('refreshChildren');
539
- }
580
+ if (this.inited) {
581
+ Fore.refreshChildren(this, true);
582
+ }
583
+ // console.timeEnd('refreshChildren');
584
+ }
540
585
 
541
- // ### refresh template expressions
542
- if (this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
543
- this._updateTemplateExpressions();
544
- this._scanForNewTemplateExpressionsNextRefresh = false; // reset
545
- }
586
+ // ### refresh template expressions
587
+ if (this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
588
+ this._updateTemplateExpressions();
589
+ this._scanForNewTemplateExpressionsNextRefresh = false; // reset
590
+ }
546
591
 
547
- this._processTemplateExpressions();
592
+ this._processTemplateExpressions();
548
593
 
549
- // console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
550
- // this.dispatchEvent(new CustomEvent('refresh-done'));
551
- // this.initialRun = false;
552
- this.style.visibility='visible';
553
- console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
554
- Fore.dispatch(this, 'refresh-done', {});
594
+ // console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
595
+ // this.dispatchEvent(new CustomEvent('refresh-done'));
596
+ // this.initialRun = false;
597
+ this.style.visibility = 'visible';
598
+ console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
599
+ Fore.dispatch(this, 'refresh-done', {});
555
600
 
556
- // this.isRefreshing = true;
557
- // this.parentNode.closest('fx-fore')?.refresh(false, changedPaths);
601
+ // this.isRefreshing = true;
602
+ // this.parentNode.closest('fx-fore')?.refresh(false, changedPaths);
558
603
 
559
- const subFores = Array.from(this.querySelectorAll('fx-fore'));
560
- /*
604
+ const subFores = Array.from(this.querySelectorAll('fx-fore'));
605
+ /*
561
606
  calling the parent to refresh causes errors and inconsistent state. Also it is questionable
562
607
  if a child should actually interact with its parent in this way.
563
608
 
@@ -572,293 +617,283 @@ export class FxFore extends HTMLElement {
572
617
  // await this.parentNode.closest('fx-fore')?.refresh(false);
573
618
  }
574
619
  */
575
- for (const subFore of subFores) {
576
- // subFore.refresh(false, changedPaths);
577
- if(subFore.ready){
578
- await subFore.refresh(false);
579
- }
580
- }
581
- this.isRefreshing = false;
620
+ for (const subFore of subFores) {
621
+ // subFore.refresh(false, changedPaths);
622
+ if (subFore.ready) {
623
+ await subFore.refresh(false);
624
+ }
625
+ }
626
+ this.isRefreshing = false;
627
+ }
628
+
629
+ /**
630
+ * entry point for processing of template expression enclosed in '{}' brackets.
631
+ *
632
+ * Expressions are found with an XPath search. For each node an entry is added to the storedTemplateExpressionByNode map.
633
+ *
634
+ *
635
+ * @private
636
+ */
637
+ _updateTemplateExpressions() {
638
+ const search =
639
+ "(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::fx-model)]";
640
+
641
+ const tmplExpressions = evaluateXPathToNodes(search, this, this);
642
+ // console.log('template expressions found ', tmplExpressions);
643
+
644
+ if (!this.storedTemplateExpressions) {
645
+ this.storedTemplateExpressions = [];
582
646
  }
583
647
 
584
- /**
585
- * entry point for processing of template expression enclosed in '{}' brackets.
586
- *
587
- * Expressions are found with an XPath search. For each node an entry is added to the storedTemplateExpressionByNode map.
588
- *
589
- *
590
- * @private
591
- */
592
- _updateTemplateExpressions() {
593
- const search =
594
- "(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::fx-model)]";
595
-
596
- const tmplExpressions = evaluateXPathToNodes(search, this, this);
597
- // console.log('template expressions found ', tmplExpressions);
598
-
599
- if (!this.storedTemplateExpressions) {
600
- this.storedTemplateExpressions = [];
601
- }
602
-
603
- // console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
648
+ // console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
604
649
 
605
- /*
650
+ /*
606
651
  storing expressions and their nodes for re-evaluation
607
652
  */
608
- Array.from(tmplExpressions).forEach(node => {
609
- const ele = node.nodeType === Node.ATTRIBUTE_NODE ?
610
- node.ownerElement :
611
- node.parentNode;
612
- if (ele.closest('fx-fore') !== this) {
613
- // We found something in a sub-fore. Act like it's not there
614
- return;
615
- }
616
- if (this.storedTemplateExpressionByNode.has(node)) {
617
- // If the node is already known, do not process it twice
618
- return;
619
- }
620
- const expr = this._getTemplateExpression(node);
621
-
622
- // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
623
- if(expr){
624
- this.storedTemplateExpressionByNode.set(node, expr);
625
- }
626
- });
627
- // console.log('stored template expressions ', this.storedTemplateExpressionByNode);
628
-
629
- // TODO: Should we clean up nodes that existed but are now gone?
630
- this._processTemplateExpressions();
631
- }
632
-
633
- _processTemplateExpressions() {
634
- for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
635
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
636
- // Attribute nodes are not contained by the document, but their owner elements are!
637
- if (!XPathUtil.contains(this, node.ownerElement)) {
638
- this.storedTemplateExpressionByNode.delete(node);
639
- continue;
640
- }
641
- } else if (!XPathUtil.contains(this, node)) {
642
- // For all other nodes, if this `fore` element does not contain them, they are dead
643
- this.storedTemplateExpressionByNode.delete(node);
644
- continue;
645
- }
646
- this._processTemplateExpression({
647
- node,
648
- expr: this.storedTemplateExpressionByNode.get(node),
649
- });
653
+ Array.from(tmplExpressions).forEach(node => {
654
+ const ele = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentNode;
655
+ if (ele.closest('fx-fore') !== this) {
656
+ // We found something in a sub-fore. Act like it's not there
657
+ return;
658
+ }
659
+ if (this.storedTemplateExpressionByNode.has(node)) {
660
+ // If the node is already known, do not process it twice
661
+ return;
662
+ }
663
+ const expr = this._getTemplateExpression(node);
664
+
665
+ // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
666
+ if (expr) {
667
+ this.storedTemplateExpressionByNode.set(node, expr);
668
+ }
669
+ });
670
+ // console.log('stored template expressions ', this.storedTemplateExpressionByNode);
671
+
672
+ // TODO: Should we clean up nodes that existed but are now gone?
673
+ this._processTemplateExpressions();
674
+ }
675
+
676
+ _processTemplateExpressions() {
677
+ for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
678
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
679
+ // Attribute nodes are not contained by the document, but their owner elements are!
680
+ if (!XPathUtil.contains(this, node.ownerElement)) {
681
+ this.storedTemplateExpressionByNode.delete(node);
682
+ continue;
650
683
  }
684
+ } else if (!XPathUtil.contains(this, node)) {
685
+ // For all other nodes, if this `fore` element does not contain them, they are dead
686
+ this.storedTemplateExpressionByNode.delete(node);
687
+ continue;
688
+ }
689
+ this._processTemplateExpression({
690
+ node,
691
+ expr: this.storedTemplateExpressionByNode.get(node),
692
+ });
651
693
  }
652
-
653
- // eslint-disable-next-line class-methods-use-this
654
- _processTemplateExpression(exprObj) {
655
- // console.log('processing template expression ', exprObj);
656
-
657
- const {expr} = exprObj;
658
- const {node} = exprObj;
659
- // console.log('expr ', expr);
660
- this.evaluateTemplateExpression(expr, node);
694
+ }
695
+
696
+ // eslint-disable-next-line class-methods-use-this
697
+ _processTemplateExpression(exprObj) {
698
+ // console.log('processing template expression ', exprObj);
699
+
700
+ const { expr } = exprObj;
701
+ const { node } = exprObj;
702
+ // console.log('expr ', expr);
703
+ this.evaluateTemplateExpression(expr, node);
704
+ }
705
+
706
+ /**
707
+ * evaluate a template expression on a node either text- or attribute node.
708
+ * @param {string} expr The string to parse for expressions
709
+ * @param {Node} node the node which will get updated with evaluation result
710
+ */
711
+ evaluateTemplateExpression(expr, node) {
712
+ // ### do not evaluate template expressions with nonrelevant sections
713
+ if (node.nodeType === Node.ATTRIBUTE_NODE && node.ownerElement.closest('[nonrelevant]')) return;
714
+ if (node.nodeType === Node.TEXT_NODE && node.parentNode.closest('[nonrelevant]')) return;
715
+ if (node.nodeType === Node.ELEMENT_NODE && node.closest('[nonrelevant]')) return;
716
+
717
+ // if(node.closest('[nonrelevant]')) return;
718
+ const replaced = expr.replace(/{[^}]*}/g, match => {
719
+ if (match === '{}') return match;
720
+ const naked = match.substring(1, match.length - 1);
721
+ const inscope = getInScopeContext(node, naked);
722
+ if (!inscope) {
723
+ console.warn('no inscope context for expr', naked);
724
+ const errNode =
725
+ node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
726
+ ? node.parentNode
727
+ : node;
728
+ return match;
729
+ }
730
+ // Templates are special: they use the namespace configuration from the place where they are
731
+ // being defined
732
+ const instanceId = XPathUtil.getInstanceId(naked);
733
+
734
+ // If there is an instance referred
735
+ const inst = instanceId
736
+ ? this.getModel().getInstance(instanceId)
737
+ : this.getModel().getDefaultInstance();
738
+
739
+ try {
740
+ return evaluateXPathToString(naked, inscope, node, null, inst);
741
+ } catch (error) {
742
+ console.warn('ignoring unparseable expr', error);
743
+
744
+ return match;
745
+ }
746
+ });
747
+
748
+ // Update to the new value. Don't do it though if nothing changed to prevent iframes or
749
+ // images from reloading for example
750
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
751
+ const parent = node.ownerElement;
752
+ if (parent.getAttribute(node.nodeName) !== replaced) {
753
+ parent.setAttribute(node.nodeName, replaced);
754
+ }
755
+ } else if (node.nodeType === Node.TEXT_NODE) {
756
+ if (node.textContent !== replaced) {
757
+ node.textContent = replaced;
758
+ }
661
759
  }
662
-
663
- /**
664
- * evaluate a template expression on a node either text- or attribute node.
665
- * @param expr The string to parse for expressions
666
- * @param node the node which will get updated with evaluation result
667
- */
668
- evaluateTemplateExpression(expr, node) {
669
-
670
- // ### do not evaluate template expressions with nonrelevant sections
671
- if(node.nodeType === Node.ATTRIBUTE_NODE && node.ownerElement.closest('[nonrelevant]')) return;
672
- if(node.nodeType === Node.TEXT_NODE && node.parentNode.closest('[nonrelevant]')) return;
673
- if(node.nodeType === Node.ELEMENT_NODE && node.closest('[nonrelevant]')) return;
674
-
675
- // if(node.closest('[nonrelevant]')) return;
676
- const replaced = expr.replace(/{[^}]*}/g, match => {
677
- if (match === '{}') return match;
678
- const naked = match.substring(1, match.length - 1);
679
- const inscope = getInScopeContext(node, naked);
680
- if (!inscope) {
681
- console.warn('no inscope context for expr', naked);
682
- const errNode =
683
- node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
684
- ? node.parentNode
685
- : node;
686
- return match;
687
- }
688
- // Templates are special: they use the namespace configuration from the place where they are
689
- // being defined
690
- const instanceId = XPathUtil.getInstanceId(naked);
691
-
692
- // If there is an instance referred
693
- const inst = instanceId ? this.getModel().getInstance(instanceId) : this.getModel().getDefaultInstance();
694
-
695
- try {
696
- return evaluateXPathToString(naked, inscope, node, null, inst);
697
- } catch (error) {
698
- console.warn('ignoring unparseable expr', error);
699
-
700
- return match;
701
- }
702
- });
703
-
704
- // Update to the new value. Don't do it though if nothing changed to prevent iframes or
705
- // images from reloading for example
706
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
707
- const parent = node.ownerElement;
708
- if (parent.getAttribute(node.nodeName) !== replaced) {
709
- parent.setAttribute(node.nodeName, replaced);
710
- }
711
- } else if (node.nodeType === Node.TEXT_NODE) {
712
- if (node.textContent !== replaced) {
713
- node.textContent = replaced;
714
- }
715
- }
760
+ }
761
+
762
+ // eslint-disable-next-line class-methods-use-this
763
+ _getTemplateExpression(node) {
764
+ if (this.ignoredNodes) {
765
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
766
+ node = node.ownerElement;
767
+ }
768
+ const found = this.ignoredNodes.find(n => n.contains(node));
769
+ if (found) return null;
716
770
  }
717
-
718
- // eslint-disable-next-line class-methods-use-this
719
- _getTemplateExpression(node) {
720
- if(this.ignoredNodes){
721
- if(node.nodeType === Node.ATTRIBUTE_NODE){
722
- node = node.ownerElement;
723
- }
724
- const found = this.ignoredNodes.find( (n) => n.contains(node));
725
- if(found) return null;
726
- }
727
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
728
- return node.value;
729
- }
730
- if (node.nodeType === Node.TEXT_NODE) {
731
- return node.textContent.trim();
732
- }
733
- return null;
771
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
772
+ return node.value;
734
773
  }
735
-
736
- /**
737
- * called when `model-construct-done` event is received to
738
- * start initing the UI.
739
- *
740
- * @private
741
- */
742
- _handleModelConstructDone() {
743
- /*
744
- listening on beforeunload after model is constructed - this is to be able to evaluate a condition on the data
745
- that specifies whether or not to show confirmation.
746
- */
747
- if(this.hasAttribute('show-confirmation')){
748
- const condition = this.getAttribute('show-confirmation');
749
- if(condition
750
- && condition !== 'show-confirmation'
751
- && condition !== 'true'
752
- && condition !== ''){
753
- window.addEventListener('beforeunload', event => {
754
- const mustDisplay = evaluateXPathToBoolean(condition, this.getModel().getDefaultContext(), this)
755
- if(mustDisplay){
756
- return event.returnValue = 'are you sure';
757
- }
758
- event.preventDefault();
759
- })
760
- }else{
761
- window.addEventListener('beforeunload', event => {
762
- // if(AbstractAction.dataChanged){
763
- if(FxModel.dataChanged){
764
- return event.returnValue = 'are you sure';
765
- }
766
- event.preventDefault();
767
- })
768
- }
769
- }
770
-
771
- this._initUI();
774
+ if (node.nodeType === Node.TEXT_NODE) {
775
+ return node.textContent.trim();
772
776
  }
773
-
774
- /**
775
- * If there's no instance element found in a fx-model during init it will construct
776
- * an instance from UI bindings.
777
- *
778
- * @returns {Promise<void>}
779
- * @private
780
- */
781
- async _lazyCreateInstance() {
782
- const model = this.querySelector('fx-model');
783
-
784
- // ##### lazy creation should NOT take place if there's a parent Fore using shared instances
785
- const parentFore = this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE ? this.parentNode.closest('fx-fore'): null;
786
- if(this.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE){
787
- console.log('fragment',this.parentNode)
788
- }
789
-
790
- if(parentFore){
791
- const shared = parentFore.getModel().instances.filter(shared => shared.hasAttribute('shared'));
792
- if(shared.length !==0) return;
793
- }
794
-
795
- // still need to catch just in case...
796
- try{
797
- if (model.instances.length === 0) {
798
- // console.log('### lazy creation of instance');
799
- const generatedInstance = document.createElement('fx-instance');
800
- model.appendChild(generatedInstance);
801
-
802
- const generated = document.implementation.createDocument(null, 'data', null);
803
- // const newData = this._generateInstance(this, generated.firstElementChild);
804
- this._generateInstance(this, generated.firstElementChild);
805
- generatedInstance.instanceData = generated;
806
- model.instances.push(generatedInstance);
807
- // console.log('generatedInstance ', this.getModel().getDefaultInstanceData());
808
- Fore.dispatch(this,'instance-loaded',{instance:this});
809
- }
810
- }catch (e) {
811
- console.warn('lazyCreateInstance created an error attempting to create a document', e.message);
777
+ return null;
778
+ }
779
+
780
+ /**
781
+ * called when `model-construct-done` event is received to
782
+ * start initing the UI.
783
+ *
784
+ * @private
785
+ */
786
+ _handleModelConstructDone() {
787
+ this.markAsClean();
788
+
789
+ if (this.showConfirmation) {
790
+ window.addEventListener('beforeunload', event => {
791
+ if (this.dirtyState === dirtyStates.DIRTY) {
792
+ event.preventDefault();
793
+ return true;
812
794
  }
795
+ return false;
796
+ });
797
+ }
798
+ this._initUI();
799
+ }
800
+
801
+ /**
802
+ * If there's no instance element found in a fx-model during init it will construct
803
+ * an instance from UI bindings.
804
+ *
805
+ * @returns {Promise<void>}
806
+ * @private
807
+ */
808
+ async _lazyCreateInstance() {
809
+ const model = this.querySelector('fx-model');
810
+
811
+ // ##### lazy creation should NOT take place if there's a parent Fore using shared instances
812
+ const parentFore =
813
+ this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
814
+ ? this.parentNode.closest('fx-fore')
815
+ : null;
816
+ if (this.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
817
+ console.log('fragment', this.parentNode);
813
818
  }
814
819
 
815
- /**
816
- * @param {Element} start
817
- * @param {Element} parent
818
- */
819
- _generateInstance(start, parent) {
820
- if (start.hasAttribute('ref') && !Fore.isActionElement(start.nodeName)) {
821
- const ref = start.getAttribute('ref');
822
-
823
- if (ref.includes('/')) {
824
- // console.log('complex path to create ', ref);
825
- const steps = ref.split('/');
826
- steps.forEach(step => {
827
- // const generated = document.createElement(ref);
828
- parent = this._generateNode(parent, step, start);
829
- });
830
- } else {
831
- parent = this._generateNode(parent, ref, start);
832
- }
833
- }
820
+ if (parentFore) {
821
+ const shared = parentFore
822
+ .getModel()
823
+ .instances.filter(shared => shared.hasAttribute('shared'));
824
+ if (shared.length !== 0) return;
825
+ }
834
826
 
835
- if (start.hasChildNodes()) {
836
- const list = start.children;
837
- for (let i = 0; i < list.length; i += 1) {
838
- this._generateInstance(list[i], parent);
839
- }
840
- }
841
- return parent;
827
+ // still need to catch just in case...
828
+ try {
829
+ if (model.instances.length === 0) {
830
+ // console.log('### lazy creation of instance');
831
+ const generatedInstance = document.createElement('fx-instance');
832
+ model.appendChild(generatedInstance);
833
+
834
+ const generated = document.implementation.createDocument(null, 'data', null);
835
+ // const newData = this._generateInstance(this, generated.firstElementChild);
836
+ this._generateInstance(this, generated.firstElementChild);
837
+ generatedInstance.instanceData = generated;
838
+ model.instances.push(generatedInstance);
839
+ // console.log('generatedInstance ', this.getModel().getDefaultInstanceData());
840
+ Fore.dispatch(this, 'instance-loaded', { instance: this });
841
+ }
842
+ } catch (e) {
843
+ console.warn(
844
+ 'lazyCreateInstance created an error attempting to create a document',
845
+ e.message,
846
+ );
847
+ }
848
+ }
849
+
850
+ /**
851
+ * @param {Element} start
852
+ * @param {Element} parent
853
+ */
854
+ _generateInstance(start, parent) {
855
+ if (start.hasAttribute('ref') && !Fore.isActionElement(start.nodeName)) {
856
+ const ref = start.getAttribute('ref');
857
+
858
+ if (ref.includes('/')) {
859
+ // console.log('complex path to create ', ref);
860
+ const steps = ref.split('/');
861
+ steps.forEach(step => {
862
+ // const generated = document.createElement(ref);
863
+ parent = this._generateNode(parent, step, start);
864
+ });
865
+ } else {
866
+ parent = this._generateNode(parent, ref, start);
867
+ }
842
868
  }
843
869
 
844
- // eslint-disable-next-line class-methods-use-this
845
- _generateNode(parent, step, start) {
846
- const generated = parent.ownerDocument.createElement(step);
847
- if (start.children.length === 0) {
848
- generated.textContent = start.textContent;
849
- }
850
- parent.appendChild(generated);
851
- parent = generated;
852
- return parent;
870
+ if (start.hasChildNodes()) {
871
+ const list = start.children;
872
+ for (let i = 0; i < list.length; i += 1) {
873
+ this._generateInstance(list[i], parent);
874
+ }
875
+ }
876
+ return parent;
877
+ }
878
+
879
+ // eslint-disable-next-line class-methods-use-this
880
+ _generateNode(parent, step, start) {
881
+ const generated = parent.ownerDocument.createElement(step);
882
+ if (start.children.length === 0) {
883
+ generated.textContent = start.textContent;
853
884
  }
885
+ parent.appendChild(generated);
886
+ parent = generated;
887
+ return parent;
888
+ }
854
889
 
855
- /*
890
+ /*
856
891
  _createStep(){
857
892
 
858
893
  }
859
894
  */
860
895
 
861
- /*
896
+ /*
862
897
  _generateInstance(start, parent) {
863
898
  if (start.hasAttribute('ref')) {
864
899
  const ref = start.getAttribute('ref');
@@ -869,7 +904,6 @@ export class FxFore extends HTMLElement {
869
904
  steps.forEach(step => {
870
905
  console.log('step ', step);
871
906
 
872
-
873
907
  });
874
908
  }
875
909
 
@@ -892,25 +926,25 @@ export class FxFore extends HTMLElement {
892
926
  }
893
927
  */
894
928
 
895
- /**
896
- * Start the initialization of the UI by
897
- *
898
- * 1. checking if a instance needs to be generated
899
- * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
900
- * 3. doing a full refresh of the UI
901
- *
902
- * @returns {Promise<void>}
903
- * @private
904
- */
905
- async _initUI() {
906
- // console.log('### _initUI()');
907
- console.log(`### <<<<< _initUI '${this.id}' >>>>>`);
908
-
909
- if (!this.initialRun) return;
910
- this.classList.add('initialRun');
911
- await this._lazyCreateInstance();
929
+ /**
930
+ * Start the initialization of the UI by
931
+ *
932
+ * 1. checking if a instance needs to be generated
933
+ * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
934
+ * 3. doing a full refresh of the UI
935
+ *
936
+ * @returns {Promise<void>}
937
+ * @private
938
+ */
939
+ async _initUI() {
940
+ // console.log('### _initUI()');
941
+ console.log(`### <<<<< _initUI '${this.id}' >>>>>`);
942
+
943
+ if (!this.initialRun) return;
944
+ this.classList.add('initialRun');
945
+ await this._lazyCreateInstance();
912
946
 
913
- /*
947
+ /*
914
948
  const options = {
915
949
  root: null,
916
950
  rootMargin: '0px',
@@ -918,160 +952,158 @@ export class FxFore extends HTMLElement {
918
952
  };
919
953
  */
920
954
 
921
- // First refresh should be forced
922
- await this.refresh(true);
923
- // this.style.display='block'
924
- this.classList.add('fx-ready');
925
- document.body.classList.add('fx-ready');
926
-
927
- this.ready = true;
928
- this.initialRun = false;
929
- // console.log('### >>>>> dispatching ready >>>>>', this);
930
- console.log(`### <<<<< ${this.id} ready >>>>>`);
931
-
932
- // console.log('### modelItems: ', this.getModel().modelItems);
933
- Fore.dispatch(this, 'ready', {});
934
- // console.log('dataChanged', FxModel.dataChanged);
935
- console.timeEnd('init');
936
-
937
- this.addEventListener('dragstart', this._handleDragStart);
938
- // this.addEventListener('dragend', this._handleDragEnd);
939
- this.handleDrop = event => this._handleDrop(event);
940
- this.ownerDocument.body.addEventListener('drop', this.handleDrop);
941
- this.ownerDocument.body.addEventListener('dragover', e=>{
942
- e.preventDefault();
943
- e.stopPropagation();
944
- e.dataTransfer.dropEffect = "move";
945
- });
955
+ // First refresh should be forced
956
+ await this.refresh(true);
957
+ // this.style.display='block'
958
+ this.classList.add('fx-ready');
959
+ document.body.classList.add('fx-ready');
960
+
961
+ this.ready = true;
962
+ this.initialRun = false;
963
+ // console.log('### >>>>> dispatching ready >>>>>', this);
964
+ console.log(`### <<<<< ${this.id} ready >>>>>`);
965
+
966
+ // console.log('### modelItems: ', this.getModel().modelItems);
967
+ Fore.dispatch(this, 'ready', {});
968
+ // console.log('dataChanged', FxModel.dataChanged);
969
+ console.timeEnd('init');
970
+
971
+ this.addEventListener('dragstart', this._handleDragStart);
972
+ // this.addEventListener('dragend', this._handleDragEnd);
973
+ this.handleDrop = event => this._handleDrop(event);
974
+ this.ownerDocument.body.addEventListener('drop', this.handleDrop);
975
+ this.ownerDocument.body.addEventListener('dragover', e => {
976
+ e.preventDefault();
977
+ e.stopPropagation();
978
+ e.dataTransfer.dropEffect = 'move';
979
+ });
980
+ }
981
+
982
+ _handleDragStart(event) {
983
+ const draggedItem = event.target.closest('[draggable="true"]');
984
+ this.originalDraggedItem = draggedItem;
985
+ console.log('DRAG START', this);
986
+ if (draggedItem.getAttribute('drop-action') === 'copy') {
987
+ event.dataTransfer.dropEffect = 'copy';
988
+ event.dataTransfer.effectAllowed = 'copy';
989
+ this.draggedItem = draggedItem.cloneNode(true);
990
+ this.draggedItem.setAttribute('drop-action', 'move');
991
+ this.copiedElements.add(this.draggedItem);
992
+ } else {
993
+ event.dataTransfer.dropEffect = 'move';
994
+ event.dataTransfer.effectAllowed = 'move';
995
+ this.draggedItem = draggedItem;
946
996
  }
997
+ }
947
998
 
948
- _handleDragStart (event) {
949
- const draggedItem = event.target.closest('[draggable="true"]');
950
- this.originalDraggedItem = draggedItem;
951
- console.log('DRAG START', this);
952
- if (draggedItem.getAttribute('drop-action') === 'copy') {
953
- event.dataTransfer.dropEffect = 'copy';
954
- event.dataTransfer.effectAllowed = 'copy';
955
- this.draggedItem = draggedItem.cloneNode(true);
956
- this.draggedItem.setAttribute('drop-action', 'move');
957
- this.copiedElements.add(this.draggedItem);
958
- } else {
959
- event.dataTransfer.dropEffect = 'move';
960
- event.dataTransfer.effectAllowed = 'move';
961
- this.draggedItem = draggedItem;
962
- }
963
- }
964
-
965
- _handleDrop (event) {
966
- console.log('DROP ON BODY', this)
967
- if (!this.draggedItem) {
968
- return;
969
- }
970
- // A drop on 'body' should be a removal.
971
- if (event.dataTransfer.dropEffect === 'none') {
972
- if (this.copiedElements.has(this.originalDraggedItem)) {
973
- this.originalDraggedItem.remove();
974
- }
975
- }
976
- this.originalDraggedItem = null
977
- this.draggedItem = null;
978
- event.stopPropagation();
979
- }
980
-
981
- registerLazyElement(element) {
982
- if (this.intersectionObserver) {
983
- // console.log('registerLazyElement',element);
984
- this.intersectionObserver.observe(element);
985
- }
999
+ _handleDrop(event) {
1000
+ console.log('DROP ON BODY', this);
1001
+ if (!this.draggedItem) {
1002
+ return;
986
1003
  }
987
-
988
- unRegisterLazyElement(element) {
989
- if (this.intersectionObserver) {
990
- this.intersectionObserver.unobserve(element);
991
- }
992
- }
993
-
994
- /**
995
- *
996
- * @returns {FxModel}
997
- */
998
- getModel() {
999
- return this.querySelector('fx-model');
1000
- }
1001
-
1002
- _displayMessage(e) {
1003
- // console.log('_displayMessage',e);
1004
- const {level} = e.detail;
1005
- const msg = e.detail.message;
1006
- this._showMessage(level, msg);
1007
- e.stopPropagation();
1004
+ // A drop on 'body' should be a removal.
1005
+ if (event.dataTransfer.dropEffect === 'none') {
1006
+ if (this.copiedElements.has(this.originalDraggedItem)) {
1007
+ this.originalDraggedItem.remove();
1008
+ }
1008
1009
  }
1009
-
1010
- _displayError(e) {
1011
- // const { error } = e.detail;
1012
- const msg = e.detail.message;
1013
- // this._showMessage('modal', msg);
1014
- const toast = this.shadowRoot.querySelector('#error');
1015
- toast.showToast(msg);
1010
+ this.originalDraggedItem = null;
1011
+ this.draggedItem = null;
1012
+ event.stopPropagation();
1013
+ }
1014
+
1015
+ registerLazyElement(element) {
1016
+ if (this.intersectionObserver) {
1017
+ // console.log('registerLazyElement',element);
1018
+ this.intersectionObserver.observe(element);
1016
1019
  }
1020
+ }
1017
1021
 
1018
- _displayWarning(e){
1019
- const msg = e.detail.message;
1020
- // this._showMessage('modal', msg);
1021
- const path = XPathUtil.shortenPath(evaluateXPathToString('path()',e.target,this));
1022
- const toast = this.shadowRoot.querySelector('#warn');
1023
- toast.showToast(`WARN: ${path}:${msg}`);
1022
+ unRegisterLazyElement(element) {
1023
+ if (this.intersectionObserver) {
1024
+ this.intersectionObserver.unobserve(element);
1024
1025
  }
1025
-
1026
-
1027
- _logError(e) {
1028
- e.stopPropagation();
1029
- e.preventDefault();
1030
-
1031
- console.error('ERROR',e.detail.message);
1032
- console.error(e.detail.origin);
1033
- if(e.detail.expr){
1034
- console.error('Failing expression',e.detail.expr);
1035
- }
1036
- if(this.strict){
1037
- this._displayError(e);
1038
- }
1026
+ }
1027
+
1028
+ /**
1029
+ *
1030
+ * @returns {FxModel}
1031
+ */
1032
+ getModel() {
1033
+ return this.querySelector('fx-model');
1034
+ }
1035
+
1036
+ _displayMessage(e) {
1037
+ // console.log('_displayMessage',e);
1038
+ const { level } = e.detail;
1039
+ const msg = e.detail.message;
1040
+ this._showMessage(level, msg);
1041
+ e.stopPropagation();
1042
+ }
1043
+
1044
+ _displayError(e) {
1045
+ // const { error } = e.detail;
1046
+ const msg = e.detail.message;
1047
+ // this._showMessage('modal', msg);
1048
+ const toast = this.shadowRoot.querySelector('#error');
1049
+ toast.showToast(msg);
1050
+ }
1051
+
1052
+ _displayWarning(e) {
1053
+ const msg = e.detail.message;
1054
+ // this._showMessage('modal', msg);
1055
+ const path = XPathUtil.shortenPath(evaluateXPathToString('path()', e.target, this));
1056
+ const toast = this.shadowRoot.querySelector('#warn');
1057
+ toast.showToast(`WARN: ${path}:${msg}`);
1058
+ }
1059
+
1060
+ _logError(e) {
1061
+ e.stopPropagation();
1062
+ e.preventDefault();
1063
+
1064
+ console.error('ERROR', e.detail.message);
1065
+ console.error(e.detail.origin);
1066
+ if (e.detail.expr) {
1067
+ console.error('Failing expression', e.detail.expr);
1039
1068
  }
1040
-
1041
- _copyToClipboard(target){
1042
- console.log('copyToClipboard' , target.value)
1043
- navigator.clipboard.writeText(target.value);
1044
-
1069
+ if (this.strict) {
1070
+ this._displayError(e);
1045
1071
  }
1046
-
1047
- _showMessage(level, msg) {
1048
- if (level === 'modal') {
1049
- // this.$.messageContent.innerText = msg;
1050
- // this.$.modalMessage.open();
1051
-
1052
- this.shadowRoot.getElementById('messageContent').innerText = msg;
1053
- // this.shadowRoot.getElementById('modalMessage').open();
1054
- this.shadowRoot.getElementById('modalMessage').classList.add('show');
1055
- } else if (level === 'sticky' || level === 'error' || level === 'warn') {
1056
- // const notification = this.$.modeless;
1057
- this.shadowRoot.querySelector(`#${level}`).showToast(msg);
1058
- } else {
1059
- const toast = this.shadowRoot.querySelector('#message');
1060
- toast.showToast(msg);
1061
- }
1072
+ }
1073
+
1074
+ _copyToClipboard(target) {
1075
+ console.log('copyToClipboard', target.value);
1076
+ navigator.clipboard.writeText(target.value);
1077
+ }
1078
+
1079
+ _showMessage(level, msg) {
1080
+ if (level === 'modal') {
1081
+ // this.$.messageContent.innerText = msg;
1082
+ // this.$.modalMessage.open();
1083
+
1084
+ this.shadowRoot.getElementById('messageContent').innerText = msg;
1085
+ // this.shadowRoot.getElementById('modalMessage').open();
1086
+ this.shadowRoot.getElementById('modalMessage').classList.add('show');
1087
+ } else if (level === 'sticky' || level === 'error' || level === 'warn') {
1088
+ // const notification = this.$.modeless;
1089
+ this.shadowRoot.querySelector(`#${level}`).showToast(msg);
1090
+ } else {
1091
+ const toast = this.shadowRoot.querySelector('#message');
1092
+ toast.showToast(msg);
1062
1093
  }
1063
-
1064
- /**
1065
- * wraps the element having a 'data-ref' attribute with an fx-repeat-attributes element.
1066
- * @private
1067
- */
1068
- _createRepeatsFromAttributes() {
1069
- if(this.repeatsFromAttributesCreated) return;
1070
- const repeats = this.querySelectorAll('[data-ref]');
1071
- if(repeats){
1072
- Array.from(repeats).forEach(item =>{
1073
- if(item.closest('fx-control')) return;
1074
- /*
1094
+ }
1095
+
1096
+ /**
1097
+ * wraps the element having a 'data-ref' attribute with an fx-repeat-attributes element.
1098
+ * @private
1099
+ */
1100
+ _createRepeatsFromAttributes() {
1101
+ if (this.repeatsFromAttributesCreated) return;
1102
+ const repeats = this.querySelectorAll('[data-ref]');
1103
+ if (repeats) {
1104
+ Array.from(repeats).forEach(item => {
1105
+ if (item.closest('fx-control')) return;
1106
+ /*
1075
1107
  const parentRepeat = item.closest('fx-repeat');
1076
1108
  if(parentRepeat){
1077
1109
  this.dispatchEvent(
@@ -1085,47 +1117,46 @@ export class FxFore extends HTMLElement {
1085
1117
  }
1086
1118
  */
1087
1119
 
1088
- const table = item.parentNode.closest('table');
1089
- let host;
1090
- if(table){
1091
- host = table.cloneNode(true);
1092
- }else{
1093
- host = item.cloneNode(true);
1094
- }
1095
- // ### clone original item to move it into fx-repeat-attributes
1096
- // const host = item.cloneNode(true);
1097
-
1098
- // ### create wrapper element
1099
- const repeatFromAttr = new FxRepeatAttributes();
1100
- // const repeatFromAttr = document.createElement('fx-repeat-attributes');
1101
-
1102
- // ### copy the value of 'data-ref' to 'ref' on fx-repeat-attributes
1103
- repeatFromAttr.setAttribute('ref',item.getAttribute('data-ref'));
1104
- // item.removeAttribute('data-ref');
1120
+ const table = item.parentNode.closest('table');
1121
+ let host;
1122
+ if (table) {
1123
+ host = table.cloneNode(true);
1124
+ } else {
1125
+ host = item.cloneNode(true);
1126
+ }
1127
+ // ### clone original item to move it into fx-repeat-attributes
1128
+ // const host = item.cloneNode(true);
1105
1129
 
1106
- // ### append the cloned original element to fx-repeat-attributes
1107
- repeatFromAttr.appendChild(host);
1130
+ // ### create wrapper element
1131
+ const repeatFromAttr = new FxRepeatAttributes();
1132
+ // const repeatFromAttr = document.createElement('fx-repeat-attributes');
1108
1133
 
1109
- // ### insert fx-repeat-attributes element before element with the 'data-ref'
1110
- // repeats[0].parentNode.insertBefore(repeatFromAttr,repeats[0]);
1134
+ // ### copy the value of 'data-ref' to 'ref' on fx-repeat-attributes
1135
+ repeatFromAttr.setAttribute('ref', item.getAttribute('data-ref'));
1136
+ // item.removeAttribute('data-ref');
1111
1137
 
1112
- if(table){
1113
- table.parentNode.insertBefore(repeatFromAttr,table);
1114
- table.parentNode.removeChild(table);
1115
- }else{
1116
- item.parentNode.insertBefore(repeatFromAttr,item);
1117
- item.parentNode.removeChild(item);
1118
- }
1138
+ // ### append the cloned original element to fx-repeat-attributes
1139
+ repeatFromAttr.appendChild(host);
1119
1140
 
1120
- // ### remove original item from DOM
1121
- item.setAttribute('insertPoint','');
1141
+ // ### insert fx-repeat-attributes element before element with the 'data-ref'
1142
+ // repeats[0].parentNode.insertBefore(repeatFromAttr,repeats[0]);
1122
1143
 
1123
- });
1144
+ if (table) {
1145
+ table.parentNode.insertBefore(repeatFromAttr, table);
1146
+ table.parentNode.removeChild(table);
1147
+ } else {
1148
+ item.parentNode.insertBefore(repeatFromAttr, item);
1149
+ item.parentNode.removeChild(item);
1124
1150
  }
1125
- this.repeatsFromAttributesCreated = true;
1151
+
1152
+ // ### remove original item from DOM
1153
+ item.setAttribute('insertPoint', '');
1154
+ });
1126
1155
  }
1156
+ this.repeatsFromAttributesCreated = true;
1157
+ }
1127
1158
  }
1128
1159
 
1129
1160
  if (!customElements.get('fx-fore')) {
1130
- customElements.define('fx-fore', FxFore);
1161
+ customElements.define('fx-fore', FxFore);
1131
1162
  }