@jinntec/fore 1.0.0-4 → 1.1.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 (66) hide show
  1. package/README.md +26 -38
  2. package/dist/fore-dev.js +43 -0
  3. package/dist/fore-dev.js.map +1 -0
  4. package/dist/fore.js +37 -0
  5. package/dist/fore.js.map +1 -0
  6. package/index.js +4 -0
  7. package/package.json +42 -42
  8. package/resources/fore.css +186 -0
  9. package/resources/toastify.css +87 -0
  10. package/resources/{fore-styles.css → vars.css} +0 -292
  11. package/src/DependencyNotifyingDomFacade.js +10 -16
  12. package/src/ForeElementMixin.js +26 -19
  13. package/src/actions/abstract-action.js +30 -9
  14. package/src/actions/fx-action.js +6 -4
  15. package/src/actions/fx-append.js +8 -17
  16. package/src/actions/fx-confirm.js +5 -3
  17. package/src/actions/fx-delete.js +6 -3
  18. package/src/actions/fx-dispatch.js +9 -8
  19. package/src/actions/fx-hide.js +10 -6
  20. package/src/actions/fx-insert.js +49 -39
  21. package/src/actions/fx-message.js +3 -1
  22. package/src/actions/fx-refresh.js +15 -1
  23. package/src/actions/fx-replace.js +73 -0
  24. package/src/actions/fx-return.js +42 -0
  25. package/src/actions/fx-send.js +3 -1
  26. package/src/actions/fx-setfocus.js +37 -0
  27. package/src/actions/fx-setvalue.js +58 -51
  28. package/src/actions/fx-show.js +12 -4
  29. package/src/actions/fx-toggle.js +15 -10
  30. package/src/actions/fx-update.js +3 -1
  31. package/src/dep_graph.js +4 -2
  32. package/src/drawdown.js +67 -82
  33. package/src/fore.js +158 -12
  34. package/src/functions/fx-function.js +17 -3
  35. package/src/fx-bind.js +42 -202
  36. package/src/fx-fore.js +599 -567
  37. package/src/fx-header.js +3 -1
  38. package/src/fx-instance.js +9 -1
  39. package/src/fx-model.js +59 -23
  40. package/src/fx-submission.js +106 -48
  41. package/src/fx-var.js +7 -4
  42. package/src/getInScopeContext.js +37 -11
  43. package/src/modelitem.js +4 -4
  44. package/src/relevance.js +64 -0
  45. package/src/ui/abstract-control.js +64 -37
  46. package/src/ui/fx-alert.js +7 -1
  47. package/src/ui/fx-case.js +4 -3
  48. package/src/ui/fx-container.js +7 -1
  49. package/src/ui/fx-control.js +309 -34
  50. package/src/ui/fx-dialog.js +54 -40
  51. package/src/ui/fx-group.js +3 -1
  52. package/src/ui/fx-hint.js +4 -1
  53. package/src/ui/fx-inspector.js +120 -17
  54. package/src/ui/fx-items.js +8 -8
  55. package/src/ui/fx-output.js +16 -5
  56. package/src/ui/fx-repeat.js +33 -41
  57. package/src/ui/fx-repeatitem.js +10 -4
  58. package/src/ui/fx-switch.js +5 -3
  59. package/src/ui/fx-trigger.js +3 -1
  60. package/src/xpath-evaluation.js +621 -576
  61. package/src/xpath-util.js +15 -8
  62. package/dist/fore-all.js +0 -140
  63. package/dist/fore-debug.js +0 -140
  64. package/src/.DS_Store +0 -0
  65. package/src/actions/.DS_Store +0 -0
  66. package/src/ui/.DS_Store +0 -0
package/src/fx-fore.js CHANGED
@@ -1,10 +1,10 @@
1
- import { Fore } from './fore.js';
1
+ import {Fore} from './fore.js';
2
2
  import './fx-instance.js';
3
3
  import './fx-model.js';
4
4
  import '@jinntec/jinn-toast';
5
- import { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
5
+ import {evaluateXPathToNodes, evaluateXPathToString} from './xpath-evaluation.js';
6
6
  import getInScopeContext from './getInScopeContext.js';
7
- import { XPathUtil } from './xpath-util.js';
7
+ import {XPathUtil} from './xpath-util.js';
8
8
 
9
9
  /**
10
10
  * Main class for Fore.Outermost container element for each Fore application.
@@ -16,53 +16,56 @@ import { XPathUtil } from './xpath-util.js';
16
16
  *
17
17
  * Main responsiblities are initialization and updating of model and instances, update of UI (refresh) and global messaging.
18
18
  *
19
- *
19
+ * @event compute-exception - dispatched in case the dependency graph is cirular
20
+ * @event refresh-done - dispatched after a refresh() run
21
+ * @event ready - dispatched after Fore has fully been initialized
22
+ * @event error - dispatches error when template expression fails to evaluate
20
23
  *
21
24
  * @ts-check
22
25
  */
23
26
  export class FxFore extends HTMLElement {
24
- static get properties() {
25
- return {
26
- /**
27
- * Setting this marker attribute will refresh the UI in a lazy fashion just updating elements being
28
- * in viewport.
29
- *
30
- * this feature is still experimental and should be used with caution and extra testing
31
- */
32
- lazyRefresh: {
33
- type: Boolean,
34
- },
35
- model: {
36
- type: Object,
37
- },
38
- ready: {
39
- type: Boolean,
40
- },
41
- };
42
- }
43
-
44
- /**
45
- * attaches handlers for
46
- *
47
- * - `model-construct-done` to trigger the processing of the UI
48
- * - `message` - to display a message triggered by an fx-message action
49
- * - `error` - to display an error message
50
- * - 'compute-exception` - warn about circular dependencies in graph
51
- */
52
- constructor() {
53
- super();
54
- this.model = {};
55
- this.addEventListener('model-construct-done', this._handleModelConstructDone);
56
- this.addEventListener('message', this._displayMessage);
57
- this.addEventListener('error', this._displayError);
58
- window.addEventListener('compute-exception', e => {
59
- console.error('circular dependency: ', e);
60
- });
61
-
62
- this.ready = false;
63
- this.storedTemplateExpressionByNode = new Map();
64
-
65
- const style = `
27
+ static get properties() {
28
+ return {
29
+ /**
30
+ * Setting this marker attribute will refresh the UI in a lazy fashion just updating elements being
31
+ * in viewport.
32
+ *
33
+ * this feature is still experimental and should be used with caution and extra testing
34
+ */
35
+ lazyRefresh: {
36
+ type: Boolean,
37
+ },
38
+ model: {
39
+ type: Object,
40
+ },
41
+ ready: {
42
+ type: Boolean,
43
+ },
44
+ };
45
+ }
46
+
47
+ /**
48
+ * attaches handlers for
49
+ *
50
+ * - `model-construct-done` to trigger the processing of the UI
51
+ * - `message` - to display a message triggered by an fx-message action
52
+ * - `error` - to display an error message
53
+ * - 'compute-exception` - warn about circular dependencies in graph
54
+ */
55
+ constructor() {
56
+ super();
57
+ this.model = {};
58
+ this.addEventListener('model-construct-done', this._handleModelConstructDone);
59
+ this.addEventListener('message', this._displayMessage);
60
+ this.addEventListener('error', this._displayError);
61
+ window.addEventListener('compute-exception', e => {
62
+ console.error('circular dependency: ', e);
63
+ });
64
+
65
+ this.ready = false;
66
+ this.storedTemplateExpressionByNode = new Map();
67
+
68
+ const style = `
66
69
  :host {
67
70
  // display: none;
68
71
  height:auto;
@@ -73,10 +76,10 @@ export class FxFore extends HTMLElement {
73
76
  :host ::slotted(fx-model){
74
77
  display:none;
75
78
  }
76
- :host(.fx-ready){
77
- animation: fadein .4s forwards;
78
- display:block;
79
- }
79
+ // :host(.fx-ready){
80
+ // animation: fadein .4s forwards;
81
+ // display:block;
82
+ // }
80
83
 
81
84
  #modalMessage .dialogActions{
82
85
  text-align:center;
@@ -150,8 +153,9 @@ export class FxFore extends HTMLElement {
150
153
  }
151
154
  `;
152
155
 
153
- const html = `
156
+ const html = `
154
157
  <jinn-toast id="message" gravity="bottom" position="left"></jinn-toast>
158
+ <jinn-toast id="sticky" gravity="bottom" position="left" duration="-1" close="true" data-class="sticky-message"></jinn-toast>
155
159
  <jinn-toast id="error" text="error" duration="-1" data-class="error" close="true" position="left" gravity="bottom"></jinn-toast>
156
160
  <slot></slot>
157
161
  <div id="modalMessage" class="overlay">
@@ -163,579 +167,607 @@ export class FxFore extends HTMLElement {
163
167
  </div>
164
168
  `;
165
169
 
166
- this.attachShadow({ mode: 'open' });
167
- this.shadowRoot.innerHTML = `
170
+ this.attachShadow({mode: 'open'});
171
+ this.shadowRoot.innerHTML = `
168
172
  <style>
169
173
  ${style}
170
174
  </style>
171
175
  ${html}
172
176
  `;
173
177
 
174
- this.toRefresh = [];
175
- this.initialRun = true;
176
- this.someInstanceDataStructureChanged = false;
177
- }
178
-
179
- connectedCallback() {
180
- this.lazyRefresh = this.hasAttribute('refresh-on-view');
181
- if (this.lazyRefresh) {
182
- const options = {
183
- root: null,
184
- rootMargin: '0px',
185
- threshold: 0.3,
186
- };
187
- this.intersectionObserver = new IntersectionObserver(this.handleIntersect, options);
178
+ this.toRefresh = [];
179
+ this.initialRun = true;
180
+ this.someInstanceDataStructureChanged = false;
188
181
  }
189
182
 
190
- this.src = this.hasAttribute('src')? this.getAttribute('src'):null;
191
- if(this.src){
192
- this._loadFromSrc();
193
- return ;
183
+ connectedCallback() {
184
+ /*
185
+ document.addEventListener('ready', (e) =>{
186
+ if(e.target !== this){
187
+ // e.preventDefault();
188
+ console.log('>>> e', e);
189
+ console.log('event this', this);
190
+ // console.log('event eventPhase', e.eventPhase);
191
+ // console.log('event cancelable', e.cancelable);
192
+ console.log('event target', e.target);
193
+ console.log('event composed', e.composedPath());
194
+ console.log('<<< event stopping');
195
+ e.stopPropagation();
196
+ }else{
197
+ console.log('event proceed', this);
198
+ }
199
+ // e.stopImmediatePropagation();
200
+ },true);
201
+ */
202
+
203
+ this.lazyRefresh = this.hasAttribute('refresh-on-view');
204
+ if (this.lazyRefresh) {
205
+ const options = {
206
+ root: null,
207
+ rootMargin: '0px',
208
+ threshold: 0.3,
209
+ };
210
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersect, options);
211
+ }
212
+
213
+ this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
214
+ if (this.src) {
215
+ this._loadFromSrc();
216
+ return;
217
+ }
218
+
219
+ const slot = this.shadowRoot.querySelector('slot');
220
+ slot.addEventListener('slotchange', event => {
221
+ const children = event.target.assignedElements();
222
+ let modelElement = children.find(
223
+ modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
224
+ );
225
+ if (!modelElement) {
226
+ const generatedModel = document.createElement('fx-model');
227
+ this.appendChild(generatedModel);
228
+ modelElement = generatedModel;
229
+ }
230
+ if (!modelElement.inited) {
231
+ console.log(
232
+ `########## FORE: kick off processing for ... ${window.location.href} ##########`,
233
+ );
234
+ if (this.src) {
235
+ console.log('########## FORE: loaded from ... ', this.src, '##########');
236
+ }
237
+ modelElement.modelConstruct();
238
+ }
239
+ this.model = modelElement;
240
+ });
241
+ this.addEventListener('path-mutated', e => {
242
+ console.log('path-mutated event received', e.detail.path, e.detail.index);
243
+ this.someInstanceDataStructureChanged = true;
244
+ });
194
245
  }
195
246
 
196
- const slot = this.shadowRoot.querySelector('slot');
197
- slot.addEventListener('slotchange', event => {
198
- const children = event.target.assignedElements();
199
- let modelElement = children.find(
200
- modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
201
- );
202
- if (!modelElement) {
203
- const generatedModel = document.createElement('FX-model');
204
- this.appendChild(generatedModel);
205
- modelElement = generatedModel;
206
- }
207
- if (!modelElement.inited) {
208
- console.log(
209
- `########## FORE: kick off processing for ... ${window.location.href} ##########`,
210
- );
211
- if(this.src){
212
- console.log('########## FORE: loaded from ... ', this.src, '##########');
247
+ addToRefresh(modelItem) {
248
+ const found = this.toRefresh.find(mi => mi.path === modelItem.path);
249
+ if (!found) {
250
+ this.toRefresh.push(modelItem);
213
251
  }
214
- modelElement.modelConstruct();
215
- }
216
- this.model = modelElement;
217
- });
218
- this.addEventListener('path-mutated', (e) =>{
219
- console.log('path-mutated event received', e.detail.path, e.detail.index);
220
- this.someInstanceDataStructureChanged = true;
221
- });
222
- }
223
-
224
-
225
- addToRefresh(modelItem){
226
- const found = this.toRefresh.find(mi => mi.path === modelItem.path );
227
- if(!found){
228
- this.toRefresh.push(modelItem);
229
252
  }
230
- }
231
-
232
- /**
233
- * loads a Fore from an URL given by `src`.
234
- *
235
- * Will extract the `fx-fore` element from that target file and use and replace current `fx-fore` element with the loaded one.
236
- * @private
237
- */
238
- _loadFromSrc() {
239
- console.log('########## loading Fore from ',this.src ,'##########');
240
- fetch(this.src, {
241
- method: 'GET',
242
- mode: 'cors',
243
- credentials: 'include',
244
- headers: {
245
- 'Content-Type': 'text/html',
246
- },
247
- })
248
- .then(response => {
249
- const responseContentType = response.headers.get('content-type').toLowerCase();
250
- console.log('********** responseContentType *********', responseContentType);
251
- if (responseContentType.startsWith('text/html')) {
252
- // const htmlResponse = response.text();
253
- // return new DOMParser().parseFromString(htmlResponse, 'text/html');
254
- // return response.text();
255
- return response.text().then(result =>
256
- // console.log('xml ********', result);
257
- new DOMParser().parseFromString(result, 'text/html'),
258
- );
259
- }
260
- return 'done';
261
- })
262
- .then(data => {
263
- // const theFore = fxEvaluateXPathToFirstNode('//fx-fore', data.firstElementChild);
264
- const theFore = data.querySelector('fx-fore');
265
253
 
266
- // console.log('thefore', theFore)
267
- if(!theFore){
268
- this.dispatchEvent(new CustomEvent('error',{detail:{message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`}}));
269
- }
270
- theFore.setAttribute('from-src', this.src);
271
- this.replaceWith(theFore);
254
+ /**
255
+ * loads a Fore from an URL given by `src`.
256
+ *
257
+ * Will extract the `fx-fore` element from that target file and use and replace current `fx-fore` element with the loaded one.
258
+ * @private
259
+ */
260
+ _loadFromSrc() {
261
+ console.log('########## loading Fore from ', this.src, '##########');
262
+ fetch(this.src, {
263
+ method: 'GET',
264
+ mode: 'cors',
265
+ credentials: 'include',
266
+ headers: {
267
+ 'Content-Type': 'text/html',
268
+ },
272
269
  })
273
- .catch(error => {
274
- this.dispatchEvent(new CustomEvent('error',{detail:{message: `'${this.src}' not found or does not contain Fore element.`}}));
270
+ .then(response => {
271
+ const responseContentType = response.headers.get('content-type').toLowerCase();
272
+ console.log('********** responseContentType *********', responseContentType);
273
+ if (responseContentType.startsWith('text/html')) {
274
+ // const htmlResponse = response.text();
275
+ // return new DOMParser().parseFromString(htmlResponse, 'text/html');
276
+ // return response.text();
277
+ return response.text().then(result =>
278
+ // console.log('xml ********', result);
279
+ new DOMParser().parseFromString(result, 'text/html'),
280
+ );
281
+ }
282
+ return 'done';
283
+ })
284
+ .then(data => {
285
+ // const theFore = fxEvaluateXPathToFirstNode('//fx-fore', data.firstElementChild);
286
+ const theFore = data.querySelector('fx-fore');
287
+
288
+ // console.log('thefore', theFore)
289
+ if (!theFore) {
290
+ Fore.dispatchEvent(this, 'error', {
291
+ detail: {
292
+ message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
293
+ },
294
+ });
295
+ }
296
+ theFore.setAttribute('from-src', this.src);
297
+ this.replaceWith(theFore);
298
+ })
299
+ .catch(() => {
300
+ Fore.dispatch(this, 'error', {
301
+ message: `'${this.src}' not found or does not contain Fore element.`,
302
+ });
303
+ });
304
+ }
305
+
306
+ /**
307
+ * refreshes the UI by using IntersectionObserver API. This is the handler being called
308
+ * by the observer whenever elements come into / move out of viewport.
309
+ * @param entries
310
+ * @param observer
311
+ */
312
+ handleIntersect(entries, observer) {
313
+ console.time('refreshLazy');
314
+ entries.forEach(entry => {
315
+ const {target} = entry;
316
+
317
+ if (entry.isIntersecting) {
318
+ console.log('in view', entry);
319
+ // console.log('repeat in view entry', entry.target);
320
+ // const target = entry.target;
321
+ // if(target.hasAttribute('refresh-on-view')){
322
+ target.classList.add('loaded');
323
+ // }
324
+
325
+ // todo: too restrictive here? what if target is a usual html element? shouldn't it refresh downwards?
326
+ if (typeof target.refresh === 'function') {
327
+ console.log('refreshing target', target);
328
+ target.refresh(target, true);
329
+ } else {
330
+ console.log('refreshing children', target);
331
+ Fore.refreshChildren(target, true);
332
+ }
333
+ }
275
334
  });
276
- }
277
-
278
- /**
279
- * refreshes the UI by using IntersectionObserver API. This is the handler being called
280
- * by the observer whenever elements come into / move out of viewport.
281
- * @param entries
282
- * @param observer
283
- */
284
- handleIntersect(entries, observer) {
285
- console.time('refreshLazy');
286
- entries.forEach(entry => {
287
- const { target } = entry;
288
-
289
- if (entry.isIntersecting) {
290
- console.log('in view', entry);
291
- // console.log('repeat in view entry', entry.target);
292
- // const target = entry.target;
293
- // if(target.hasAttribute('refresh-on-view')){
294
- target.classList.add('loaded');
295
- // }
296
-
297
- // todo: too restrictive here? what if target is a usual html element? shouldn't it refresh downwards?
298
- if (typeof target.refresh === 'function') {
299
- console.log('refreshing target', target);
300
- target.refresh(target, true);
335
+ entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
336
+
337
+ console.timeEnd('refreshLazy');
338
+ }
339
+
340
+ evaluateToNodes(xpath, context) {
341
+ return evaluateXPathToNodes(xpath, context, this);
342
+ }
343
+
344
+ disconnectedCallback() {
345
+ /*
346
+ this.removeEventListener('model-construct-done', this._handleModelConstructDone);
347
+ this.removeEventListener('message', this._displayMessage);
348
+ this.removeEventListener('error', this._displayError);
349
+ this.storedTemplateExpressionByNode=null;
350
+ this.shadowRoot = undefined;
351
+ */
352
+ }
353
+
354
+ /**
355
+ * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
356
+ * the bound modelItem to the bound element.
357
+ *
358
+ *
359
+ * force - boolean - if true will refresh all children disregarding toRefresh array
360
+ *
361
+ */
362
+ async forceRefresh() {
363
+ console.time('refresh');
364
+ console.group('### forced refresh', this);
365
+
366
+ Fore.refreshChildren(this, true);
367
+ this._updateTemplateExpressions();
368
+ this.someInstanceDataStructureChanged = false; // reset
369
+ this._processTemplateExpressions();
370
+ Fore.dispatch(this, 'refresh-done', {});
371
+
372
+ console.groupEnd();
373
+ console.timeEnd('refresh');
374
+ }
375
+
376
+ async refresh(force) {
377
+ // refresh () {
378
+ console.group('### refresh');
379
+
380
+ console.time('refresh');
381
+
382
+ // ### refresh Fore UI elements
383
+ console.time('refreshChildren');
384
+ console.log('toRefresh', this.toRefresh);
385
+
386
+ // if (!this.initialRun && this.toRefresh.length !== 0) {
387
+ if (!this.initialRun && this.toRefresh.length !== 0) {
388
+ let needsRefresh = false;
389
+
390
+ // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
391
+ this.toRefresh.forEach(modelItem => {
392
+ // check if modelItem has boundControls - if so, call refresh() for each of them
393
+ const controlsToRefresh = modelItem.boundControls;
394
+ if (controlsToRefresh) {
395
+ controlsToRefresh.forEach(ctrl => {
396
+ ctrl.refresh();
397
+ });
398
+ }
399
+
400
+ // ### check if other controls depend on current modelItem
401
+ const {mainGraph} = this.getModel();
402
+ if (mainGraph && mainGraph.hasNode(modelItem.path)) {
403
+ const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
404
+ // ### iterate dependant modelItems and refresh all their boundControls
405
+ if (deps.length !== 0) {
406
+ deps.forEach(dep => {
407
+ // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
408
+ const basePath = XPathUtil.getBasePath(dep);
409
+ const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
410
+ // ### refresh all boundControls
411
+ modelItemOfDep.boundControls.forEach(control => {
412
+ control.refresh();
413
+ });
414
+ });
415
+ needsRefresh = true;
416
+ }
417
+ }
418
+ });
419
+ this.toRefresh = [];
420
+ if (!needsRefresh) {
421
+ console.log('skipping refresh - no dependants');
422
+ }
301
423
  } else {
302
- console.log('refreshing children', target);
303
- Fore.refreshChildren(target, true);
424
+ Fore.refreshChildren(this, true);
425
+ console.timeEnd('refreshChildren');
304
426
  }
305
- }
306
- });
307
- entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
308
427
 
309
- console.timeEnd('refreshLazy');
310
- }
428
+ // ### refresh template expressions
429
+ if (this.initialRun || this.someInstanceDataStructureChanged) {
430
+ this._updateTemplateExpressions();
431
+ this.someInstanceDataStructureChanged = false; // reset
432
+ }
433
+ this._processTemplateExpressions();
311
434
 
312
- evaluateToNodes(xpath, context) {
313
- return evaluateXPathToNodes(xpath, context, this);
314
- }
435
+ console.timeEnd('refresh');
315
436
 
316
- disconnectedCallback() {
317
- /*
318
- this.removeEventListener('model-construct-done', this._handleModelConstructDone);
319
- this.removeEventListener('message', this._displayMessage);
320
- this.removeEventListener('error', this._displayError);
321
- this.storedTemplateExpressionByNode=null;
322
- this.shadowRoot = undefined;
323
- */
324
- }
325
-
326
- /**
327
- * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
328
- * the bound modelItem to the bound element.
329
- *
330
- *
331
- * AVT:
332
- *
333
- */
334
- async refresh(force) {
335
- // refresh () {
336
- console.group('### refresh');
337
-
338
- console.time('refresh');
339
-
340
- // ### refresh Fore UI elements
341
- console.time('refreshChildren');
342
- console.log('toRefresh',this.toRefresh);
343
-
344
- if(!this.initialRun && this.toRefresh.length !== 0){
345
- let needsRefresh = false;
346
-
347
- // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
348
- this.toRefresh.forEach(modelItem => {
349
- // check if modelItem has boundControls - if so, call refresh() for each of them
350
- const controlsToRefresh = modelItem.boundControls;
351
- if(controlsToRefresh){
352
- controlsToRefresh.forEach(ctrl => {
353
- ctrl.refresh();
354
- });
437
+ console.groupEnd();
438
+ // console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
439
+ // this.dispatchEvent(new CustomEvent('refresh-done'));
440
+ Fore.dispatch(this, 'refresh-done', {});
441
+ }
442
+
443
+ /**
444
+ * entry point for processing of template expression enclosed in '{}' brackets.
445
+ *
446
+ * Expressions are found with an XPath search. For each node an entry is added to the storedTemplateExpressionByNode map.
447
+ *
448
+ *
449
+ * @private
450
+ */
451
+ _updateTemplateExpressions() {
452
+ // Note the fact we're going over HTML here: therefore the `html` prefix.
453
+ const search =
454
+ "(descendant-or-self::*/(text(), @*))[not(ancestor-or-self::fx-model)][matches(.,'\\{.*\\}')]";
455
+
456
+ const tmplExpressions = evaluateXPathToNodes(search, this, this);
457
+ // console.log('template expressions found ', tmplExpressions);
458
+
459
+ if (!this.storedTemplateExpressions) {
460
+ this.storedTemplateExpressions = [];
355
461
  }
356
462
 
357
- // ### check if other controls depend on current modelItem
358
- const mainGraph = this.getModel().mainGraph;
359
- if(mainGraph && mainGraph.hasNode(modelItem.path)){
360
- const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
361
- // ### iterate dependant modelItems and refresh all their boundControls
362
- if(deps.length !== 0){
363
- deps.forEach(dep => {
364
- // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
365
- const basePath = XPathUtil.getBasePath(dep);
366
- const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
367
- // ### refresh all boundControls
368
- modelItemOfDep.boundControls.forEach(control =>{control.refresh()});
463
+ console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
464
+
465
+ /*
466
+ storing expressions and their nodes for re-evaluation
467
+ */
468
+ Array.from(tmplExpressions).forEach(node => {
469
+ if (this.storedTemplateExpressionByNode.has(node)) {
470
+ // If the node is already known, do not process it twice
471
+ return;
472
+ }
473
+ const expr = this._getTemplateExpression(node);
474
+
475
+ // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
476
+ this.storedTemplateExpressionByNode.set(node, expr);
477
+ });
478
+ console.log('stored template expressions ', this.storedTemplateExpressionByNode);
479
+
480
+ // TODO: Should we clean up nodes that existed but are now gone?
481
+ this._processTemplateExpressions();
482
+ }
483
+
484
+ _processTemplateExpressions() {
485
+ for (const node of this.storedTemplateExpressionByNode.keys()) {
486
+ this._processTemplateExpression({
487
+ node,
488
+ expr: this.storedTemplateExpressionByNode.get(node),
369
489
  });
370
- needsRefresh = true;
371
- }
372
490
  }
373
- });
374
- this.toRefresh = [];
375
- if(!needsRefresh){
376
- console.log('skipping refresh - no dependants');
377
- }
378
- }else{
379
- Fore.refreshChildren(this, true);
380
- console.timeEnd('refreshChildren');
381
491
  }
382
492
 
383
- // ### refresh template expressions
384
- if(this.initialRun || this.someInstanceDataStructureChanged){
385
- this._updateTemplateExpressions();
386
- this.someInstanceDataStructureChanged = false; //reset
387
- }
388
- this._processTemplateExpressions();
389
-
390
- console.timeEnd('refresh');
391
-
392
- console.groupEnd();
393
- console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
394
- this.dispatchEvent(new CustomEvent('refresh-done'));
395
- }
396
-
397
- /**
398
- * entry point for processing of template expression enclosed in '{}' brackets.
399
- *
400
- * Expressions are found with an XPath search. For each node an entry is added to the storedTemplateExpressionByNode map.
401
- *
402
- *
403
- * @private
404
- */
405
- _updateTemplateExpressions() {
406
- // Note the fact we're going over HTML here: therefore the `html` prefix.
407
- const search =
408
- "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::fx-model/descendant-or-self::node()/(., @*)";
409
-
410
- const tmplExpressions = evaluateXPathToNodes(search, this, this);
411
- console.log('template expressions found ', tmplExpressions);
412
-
413
- if (!this.storedTemplateExpressions) {
414
- this.storedTemplateExpressions = [];
493
+ // eslint-disable-next-line class-methods-use-this
494
+ _processTemplateExpression(exprObj) {
495
+ // console.log('processing template expression ', exprObj);
496
+
497
+ const {expr} = exprObj;
498
+ const {node} = exprObj;
499
+ // console.log('expr ', expr);
500
+ this.evaluateTemplateExpression(expr, node, this);
415
501
  }
416
502
 
417
- console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
503
+ /**
504
+ * evaluate a template expression (some expression in {} brackets) on a node (either text- or attribute node.
505
+ * @param input The string to parse for expressions
506
+ * @param node the node which will get updated with evaluation result
507
+ * @param form the form element
508
+ */
509
+ evaluateTemplateExpression(expr, node) {
510
+ const replaced = expr.replace(/{[^}]*}/g, match => {
511
+ if (match === '{}') return match;
512
+ const naked = match.substring(1, match.length - 1);
513
+ const inscope = getInScopeContext(node, naked);
514
+ if (!inscope) {
515
+ const errNode =
516
+ node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
517
+ ? node.parentNode
518
+ : node;
519
+ console.warn('no inscope context for ', errNode);
520
+ return match;
521
+ }
522
+ // Templates are special: they use the namespace configuration from the place where they are
523
+ // being defined
524
+ const instanceId = XPathUtil.getInstanceId(naked);
525
+ const inst = this.getModel().getInstance(instanceId);
526
+ try {
527
+ return evaluateXPathToString(naked, inscope, node, null, inst);
528
+ } catch (error) {
529
+ console.log('ignoring unparseable expr');
530
+ return match;
531
+ }
532
+ });
418
533
 
419
- /*
420
- storing expressions and their nodes for re-evaluation
421
- */
422
- Array.from(tmplExpressions).forEach(node => {
423
- if (this.storedTemplateExpressionByNode.has(node)) {
424
- // If the node is already known, do not process it twice
425
- return;
426
- }
427
- const expr = this._getTemplateExpression(node);
428
-
429
- // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
430
- this.storedTemplateExpressionByNode.set(node, expr);
431
- });
432
- console.log('stored template expressions ', this.storedTemplateExpressionByNode);
433
-
434
- // TODO: Should we clean up nodes that existed but are now gone?
435
- this._processTemplateExpressions();
436
-
437
- }
438
-
439
- _processTemplateExpressions() {
440
- for (const node of this.storedTemplateExpressionByNode.keys()) {
441
- this._processTemplateExpression({
442
- node,
443
- expr: this.storedTemplateExpressionByNode.get(node),
444
- });
445
- }
446
- }
447
-
448
- // eslint-disable-next-line class-methods-use-this
449
- _processTemplateExpression(exprObj) {
450
- // console.log('processing template expression ', exprObj);
451
-
452
- const { expr } = exprObj;
453
- const { node } = exprObj;
454
- // console.log('expr ', expr);
455
- this.evaluateTemplateExpression(expr, node, this);
456
- }
457
-
458
- /**
459
- * evaluate a template expression (some expression in {} brackets) on a node (either text- or attribute node.
460
- * @param expr the XPath to evaluate
461
- * @param node the node which will get updated with evaluation result
462
- * @param form the form element
463
- */
464
- evaluateTemplateExpression(expr, node) {
465
- if (expr === '{}') return;
466
- const matches = expr.match(/{[^}]*}/g);
467
- const namespaceContextNode =
468
- node.nodeType === node.TEXT_NODE ? node.parentNode : node.ownerElement;
469
- if (matches) {
470
- matches.forEach(match => {
471
- // console.log('match ', match);
472
- let naked = match.substring(1, match.length - 1);
473
- const inscope = getInScopeContext(node, naked);
474
- if (!inscope) {
475
- const errNode =
476
- node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
477
- ? node.parentNode
478
- : node;
479
- console.warn('no inscope context for ', errNode);
480
- return;
481
- }
482
- // Templates are special: they use the namespace configuration from the place where they are
483
- // being defined
484
- const instanceId = XPathUtil.getInstanceId(naked);
485
- // console.log('target instance ', instanceId);
486
- const inst = this.getModel().getInstance(instanceId);
487
- try {
488
- const result = evaluateXPathToString(naked, inscope, node, null, inst);
489
-
490
- // console.log('result of eval ', result);
491
- const replaced = expr.replaceAll(match, result);
492
- // console.log('result of replacing ', replaced);
493
-
494
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
534
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
495
535
  const parent = node.ownerElement;
496
-
497
- // parent.setAttribute(name, replaced);
498
536
  parent.setAttribute(node.nodeName, replaced);
499
- } else if (node.nodeType === Node.TEXT_NODE) {
537
+ } else if (node.nodeType === Node.TEXT_NODE) {
500
538
  node.textContent = replaced;
501
- }
502
-
503
- if (replaced.includes('{')) {
504
- // console.log('need to go next round');
505
-
506
- // todo: duplicated code here - see above
507
- naked = replaced.substring(1, replaced.length);
508
- this.evaluateTemplateExpression(replaced, node);
509
- }
510
- } catch (error) {
511
- this.dispatchEvent(new CustomEvent('error', { detail: error }));
512
539
  }
513
- });
514
540
  }
515
- }
516
541
 
517
- // eslint-disable-next-line class-methods-use-this
518
- _getTemplateExpression(node) {
519
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
520
- return node.value;
521
- }
522
- if (node.nodeType === Node.TEXT_NODE) {
523
- return node.textContent.trim();
542
+ // eslint-disable-next-line class-methods-use-this
543
+ _getTemplateExpression(node) {
544
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
545
+ return node.value;
546
+ }
547
+ if (node.nodeType === Node.TEXT_NODE) {
548
+ return node.textContent.trim();
549
+ }
550
+ return null;
524
551
  }
525
- return null;
526
- }
527
-
528
- /**
529
- * called when `model-construct-done` event is received to
530
- * start initing of the UI.
531
- *
532
- * @private
533
- */
534
- _handleModelConstructDone() {
535
- this._initUI();
536
- }
537
-
538
- /**
539
- * If there's no instance element found in a fx-model during init it will construct
540
- * an instance from UI bindings.
541
- *
542
- * @returns {Promise<void>}
543
- * @private
544
- */
545
- async _lazyCreateInstance() {
546
- const model = this.querySelector('fx-model');
547
- if (model.instances.length === 0) {
548
- console.log('### lazy creation of instance');
549
- const generatedInstance = document.createElement('fx-instance');
550
- model.appendChild(generatedInstance);
551
-
552
- const generated = document.implementation.createDocument(null, 'data', null);
553
- // const newData = this._generateInstance(this, generated.firstElementChild);
554
- this._generateInstance(this, generated.firstElementChild);
555
- generatedInstance.instanceData = generated;
556
- model.instances.push(generatedInstance);
557
- console.log('generatedInstance ', this.getModel().getDefaultInstanceData());
552
+
553
+ /**
554
+ * called when `model-construct-done` event is received to
555
+ * start initing of the UI.
556
+ *
557
+ * @private
558
+ */
559
+ _handleModelConstructDone() {
560
+ this._initUI();
558
561
  }
559
- }
560
-
561
- /**
562
- * @param {Element} start
563
- * @param {Element} parent
564
- */
565
- _generateInstance(start, parent) {
566
- if (start.hasAttribute('ref') && !Fore.isActionElement(start.nodeName)) {
567
- const ref = start.getAttribute('ref');
568
-
569
- if (ref.includes('/')) {
570
- console.log('complex path to create ', ref);
571
- const steps = ref.split('/');
572
- steps.forEach(step => {
573
- console.log('step ', step);
574
-
575
- // const generated = document.createElement(ref);
576
- parent = this._generateNode(parent, step, start);
577
- });
578
- } else {
579
- parent = this._generateNode(parent, ref, start);
580
- }
562
+
563
+ /**
564
+ * If there's no instance element found in a fx-model during init it will construct
565
+ * an instance from UI bindings.
566
+ *
567
+ * @returns {Promise<void>}
568
+ * @private
569
+ */
570
+ async _lazyCreateInstance() {
571
+ const model = this.querySelector('fx-model');
572
+ if (model.instances.length === 0) {
573
+ console.log('### lazy creation of instance');
574
+ const generatedInstance = document.createElement('fx-instance');
575
+ model.appendChild(generatedInstance);
576
+
577
+ const generated = document.implementation.createDocument(null, 'data', null);
578
+ // const newData = this._generateInstance(this, generated.firstElementChild);
579
+ this._generateInstance(this, generated.firstElementChild);
580
+ generatedInstance.instanceData = generated;
581
+ model.instances.push(generatedInstance);
582
+ console.log('generatedInstance ', this.getModel().getDefaultInstanceData());
583
+ }
581
584
  }
582
585
 
583
- if (start.hasChildNodes()) {
584
- const list = start.children;
585
- for (let i = 0; i < list.length; i += 1) {
586
- this._generateInstance(list[i], parent);
587
- }
586
+ /**
587
+ * @param {Element} start
588
+ * @param {Element} parent
589
+ */
590
+ _generateInstance(start, parent) {
591
+ if (start.hasAttribute('ref') && !Fore.isActionElement(start.nodeName)) {
592
+ const ref = start.getAttribute('ref');
593
+
594
+ if (ref.includes('/')) {
595
+ console.log('complex path to create ', ref);
596
+ const steps = ref.split('/');
597
+ steps.forEach(step => {
598
+ console.log('step ', step);
599
+
600
+ // const generated = document.createElement(ref);
601
+ parent = this._generateNode(parent, step, start);
602
+ });
603
+ } else {
604
+ parent = this._generateNode(parent, ref, start);
605
+ }
606
+ }
607
+
608
+ if (start.hasChildNodes()) {
609
+ const list = start.children;
610
+ for (let i = 0; i < list.length; i += 1) {
611
+ this._generateInstance(list[i], parent);
612
+ }
613
+ }
614
+ return parent;
588
615
  }
589
- return parent;
590
- }
591
-
592
- // eslint-disable-next-line class-methods-use-this
593
- _generateNode(parent, step, start) {
594
- const generated = parent.ownerDocument.createElement(step);
595
- if (start.children.length === 0) {
596
- generated.textContent = start.textContent;
616
+
617
+ // eslint-disable-next-line class-methods-use-this
618
+ _generateNode(parent, step, start) {
619
+ const generated = parent.ownerDocument.createElement(step);
620
+ if (start.children.length === 0) {
621
+ generated.textContent = start.textContent;
622
+ }
623
+ parent.appendChild(generated);
624
+ parent = generated;
625
+ return parent;
597
626
  }
598
- parent.appendChild(generated);
599
- parent = generated;
600
- return parent;
601
- }
602
627
 
603
- /*
604
- _createStep(){
628
+ /*
629
+ _createStep(){
605
630
 
606
- }
607
- */
631
+ }
632
+ */
608
633
 
609
- /*
610
- _generateInstance(start, parent) {
611
- if (start.hasAttribute('ref')) {
612
- const ref = start.getAttribute('ref');
634
+ /*
635
+ _generateInstance(start, parent) {
636
+ if (start.hasAttribute('ref')) {
637
+ const ref = start.getAttribute('ref');
613
638
 
614
- if(ref.includes('/')){
615
- console.log('complex path to create ', ref);
616
- const steps = ref.split('/');
617
- steps.forEach(step => {
618
- console.log('step ', step);
639
+ if(ref.includes('/')){
640
+ console.log('complex path to create ', ref);
641
+ const steps = ref.split('/');
642
+ steps.forEach(step => {
643
+ console.log('step ', step);
619
644
 
620
645
 
621
- });
622
- }
646
+ });
647
+ }
623
648
 
624
- // const generated = document.createElement(ref);
625
- const generated = parent.ownerDocument.createElement(ref);
626
- if (start.children.length === 0) {
627
- generated.textContent = start.textContent;
649
+ // const generated = document.createElement(ref);
650
+ const generated = parent.ownerDocument.createElement(ref);
651
+ if (start.children.length === 0) {
652
+ generated.textContent = start.textContent;
653
+ }
654
+ parent.appendChild(generated);
655
+ parent = generated;
628
656
  }
629
- parent.appendChild(generated);
630
- parent = generated;
631
- }
632
657
 
633
- if (start.hasChildNodes()) {
634
- const list = start.children;
635
- for (let i = 0; i < list.length; i += 1) {
636
- this._generateInstance(list[i], parent);
658
+ if (start.hasChildNodes()) {
659
+ const list = start.children;
660
+ for (let i = 0; i < list.length; i += 1) {
661
+ this._generateInstance(list[i], parent);
662
+ }
637
663
  }
664
+ return parent;
638
665
  }
639
- return parent;
666
+ */
667
+
668
+ /**
669
+ * Start the initialization of the UI by
670
+ *
671
+ * 1. checking if a instance needs to be generated
672
+ * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
673
+ * 3. doing a full refresh of the UI
674
+ *
675
+ * @returns {Promise<void>}
676
+ * @private
677
+ */
678
+ async _initUI() {
679
+ console.log('### _initUI()');
680
+ if (!this.initialRun) return;
681
+ await this._lazyCreateInstance();
682
+
683
+ // console.log('registering variables!');
684
+ const variables = new Map();
685
+ (function registerVariables(node) {
686
+ for (const child of node.children) {
687
+ if ('setInScopeVariables' in child) {
688
+ child.setInScopeVariables(variables);
689
+ }
690
+ registerVariables(child);
691
+ }
692
+ })(this);
693
+ console.log('Found variables:', variables);
694
+
695
+ /*
696
+ const options = {
697
+ root: null,
698
+ rootMargin: '0px',
699
+ threshold: 0.3,
700
+ };
701
+ */
702
+
703
+ await this.refresh();
704
+ // this.style.display='block'
705
+ this.classList.add('fx-ready');
706
+ document.body.classList.add('fx-ready');
707
+
708
+ this.ready = true;
709
+ this.initialRun = false;
710
+ // console.log('### >>>>> dispatching ready >>>>>', this);
711
+ console.log('modelItems: ', this.getModel().modelItems);
712
+ console.log('### <<<<< FORE: form fully initialized...', this);
713
+ Fore.dispatch(this, 'ready', {});
714
+ }
715
+
716
+ registerLazyElement(element) {
717
+ if (this.intersectionObserver) {
718
+ // console.log('registerLazyElement',element);
719
+ this.intersectionObserver.observe(element);
640
720
  }
641
- */
642
-
643
- /**
644
- * Start the initialization of the UI by
645
- *
646
- * 1. checking if a instance needs to be generated
647
- * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
648
- * 3. doing a full refresh of the UI
649
- *
650
- * @returns {Promise<void>}
651
- * @private
652
- */
653
- async _initUI() {
654
- console.log('### _initUI()');
655
-
656
- await this._lazyCreateInstance();
657
-
658
- console.log('registering variables!');
659
- const variables = new Map();
660
- (function registerVariables(node) {
661
- for (const child of node.children) {
662
- if ('setInScopeVariables' in child) {
663
- child.setInScopeVariables(variables);
721
+ }
722
+
723
+ unRegisterLazyElement(element) {
724
+ if (this.intersectionObserver) {
725
+ this.intersectionObserver.unobserve(element);
664
726
  }
665
- registerVariables(child);
666
- }
667
- })(this);
668
- console.log('Found variables:', variables);
669
-
670
- const options = {
671
- root: null,
672
- rootMargin: '0px',
673
- threshold: 0.3,
674
- };
675
-
676
- await this.refresh();
677
- // this.style.display='block'
678
- this.classList.add('fx-ready');
679
-
680
- this.ready = true;
681
- this.initialRun = false;
682
- console.log('### <<<<< dispatching ready >>>>>');
683
- console.log('########## modelItems: ', this.getModel().modelItems);
684
- console.log('########## FORE: form fully initialized... ##########');
685
- this.dispatchEvent(new CustomEvent('ready', {}));
686
- }
687
-
688
- registerLazyElement(element) {
689
- if (this.intersectionObserver) {
690
- // console.log('registerLazyElement',element);
691
- this.intersectionObserver.observe(element);
692
727
  }
693
- }
694
728
 
695
- unRegisterLazyElement(element) {
696
- if (this.intersectionObserver) {
697
- this.intersectionObserver.unobserve(element);
729
+ /**
730
+ *
731
+ * @returns {FxModel}
732
+ */
733
+ getModel() {
734
+ return this.querySelector('fx-model');
698
735
  }
699
- }
700
-
701
- /**
702
- *
703
- * @returns {FxModel}
704
- */
705
- getModel() {
706
- return this.querySelector('fx-model');
707
- }
708
-
709
- _displayMessage(e) {
710
- const { level } = e.detail;
711
- const msg = e.detail.message;
712
- this._showMessage(level, msg);
713
- }
714
-
715
- _displayError(e) {
716
- // const { error } = e.detail;
717
- const msg = e.detail.message;
718
- // this._showMessage('modal', msg);
719
- const toast = this.shadowRoot.querySelector('#error');
720
- toast.showToast(msg);
721
- }
722
-
723
- _showMessage(level, msg) {
724
- if (level === 'modal') {
725
- // this.$.messageContent.innerText = msg;
726
- // this.$.modalMessage.open();
727
-
728
- this.shadowRoot.getElementById('messageContent').innerText = msg;
729
- // this.shadowRoot.getElementById('modalMessage').open();
730
- this.shadowRoot.getElementById('modalMessage').classList.add('show');
731
- } else if (level === 'modeless') {
732
- // const notification = this.$.modeless;
733
- this.shadowRoot.querySelector('#message').showToast(msg);
734
- } else {
735
- const toast = this.shadowRoot.querySelector('#message');
736
- toast.showToast(msg);
736
+
737
+ _displayMessage(e) {
738
+ // console.log('_displayMessage',e);
739
+ const {level} = e.detail;
740
+ const msg = e.detail.message;
741
+ this._showMessage(level, msg);
742
+ e.stopPropagation();
743
+ }
744
+
745
+ _displayError(e) {
746
+ // const { error } = e.detail;
747
+ const msg = e.detail.message;
748
+ // this._showMessage('modal', msg);
749
+ const toast = this.shadowRoot.querySelector('#error');
750
+ toast.showToast(msg);
751
+ }
752
+
753
+ _showMessage(level, msg) {
754
+ if (level === 'modal') {
755
+ // this.$.messageContent.innerText = msg;
756
+ // this.$.modalMessage.open();
757
+
758
+ this.shadowRoot.getElementById('messageContent').innerText = msg;
759
+ // this.shadowRoot.getElementById('modalMessage').open();
760
+ this.shadowRoot.getElementById('modalMessage').classList.add('show');
761
+ } else if (level === 'sticky') {
762
+ // const notification = this.$.modeless;
763
+ this.shadowRoot.querySelector('#sticky').showToast(msg);
764
+ } else {
765
+ const toast = this.shadowRoot.querySelector('#message');
766
+ toast.showToast(msg);
767
+ }
737
768
  }
738
- }
739
769
  }
740
770
 
741
- customElements.define('fx-fore', FxFore);
771
+ if (!customElements.get('fx-fore')) {
772
+ customElements.define('fx-fore', FxFore);
773
+ }