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