@jinntec/fore 1.0.0 → 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.
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.
@@ -24,48 +24,48 @@ import { XPathUtil } from './xpath-util.js';
24
24
  * @ts-check
25
25
  */
26
26
  export class FxFore extends HTMLElement {
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 = `
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 = `
69
69
  :host {
70
70
  // display: none;
71
71
  height:auto;
@@ -153,7 +153,7 @@ export class FxFore extends HTMLElement {
153
153
  }
154
154
  `;
155
155
 
156
- const html = `
156
+ const html = `
157
157
  <jinn-toast id="message" gravity="bottom" position="left"></jinn-toast>
158
158
  <jinn-toast id="sticky" gravity="bottom" position="left" duration="-1" close="true" data-class="sticky-message"></jinn-toast>
159
159
  <jinn-toast id="error" text="error" duration="-1" data-class="error" close="true" position="left" gravity="bottom"></jinn-toast>
@@ -167,610 +167,607 @@ export class FxFore extends HTMLElement {
167
167
  </div>
168
168
  `;
169
169
 
170
- this.attachShadow({ mode: 'open' });
171
- this.shadowRoot.innerHTML = `
170
+ this.attachShadow({mode: 'open'});
171
+ this.shadowRoot.innerHTML = `
172
172
  <style>
173
173
  ${style}
174
174
  </style>
175
175
  ${html}
176
176
  `;
177
177
 
178
- this.toRefresh = [];
179
- this.initialRun = true;
180
- this.someInstanceDataStructureChanged = false;
181
- }
182
-
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);
178
+ this.toRefresh = [];
179
+ this.initialRun = true;
180
+ this.someInstanceDataStructureChanged = false;
211
181
  }
212
182
 
213
- this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
214
- if (this.src) {
215
- this._loadFromSrc();
216
- return;
217
- }
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
+ }
218
212
 
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
- );
213
+ this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
234
214
  if (this.src) {
235
- console.log('########## FORE: loaded from ... ', this.src, '##########');
215
+ this._loadFromSrc();
216
+ return;
236
217
  }
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
- });
245
- }
246
-
247
- addToRefresh(modelItem) {
248
- const found = this.toRefresh.find(mi => mi.path === modelItem.path);
249
- if (!found) {
250
- this.toRefresh.push(modelItem);
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
+ });
251
245
  }
252
- }
253
-
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
- },
269
- })
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
- );
246
+
247
+ addToRefresh(modelItem) {
248
+ const found = this.toRefresh.find(mi => mi.path === modelItem.path);
249
+ if (!found) {
250
+ this.toRefresh.push(modelItem);
281
251
  }
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?`,
252
+ }
253
+
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',
293
268
  },
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.`,
269
+ })
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
+ }
302
334
  });
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);
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
+ }
329
423
  } else {
330
- console.log('refreshing children', target);
331
- Fore.refreshChildren(target, true);
424
+ Fore.refreshChildren(this, true);
425
+ console.timeEnd('refreshChildren');
332
426
  }
333
- }
334
- });
335
- entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
336
427
 
337
- console.timeEnd('refreshLazy');
338
- }
428
+ // ### refresh template expressions
429
+ if (this.initialRun || this.someInstanceDataStructureChanged) {
430
+ this._updateTemplateExpressions();
431
+ this.someInstanceDataStructureChanged = false; // reset
432
+ }
433
+ this._processTemplateExpressions();
339
434
 
340
- evaluateToNodes(xpath, context) {
341
- return evaluateXPathToNodes(xpath, context, this);
342
- }
435
+ console.timeEnd('refresh');
343
436
 
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
- * AVT:
360
- *
361
- */
362
- async refresh() {
363
- // refresh () {
364
- console.group('### refresh');
365
-
366
- console.time('refresh');
367
-
368
- // ### refresh Fore UI elements
369
- console.time('refreshChildren');
370
- console.log('toRefresh', this.toRefresh);
371
-
372
- if (!this.initialRun && this.toRefresh.length !== 0) {
373
- let needsRefresh = false;
374
-
375
- // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
376
- this.toRefresh.forEach(modelItem => {
377
- // check if modelItem has boundControls - if so, call refresh() for each of them
378
- const controlsToRefresh = modelItem.boundControls;
379
- if (controlsToRefresh) {
380
- controlsToRefresh.forEach(ctrl => {
381
- ctrl.refresh();
382
- });
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 = [];
383
461
  }
384
462
 
385
- // ### check if other controls depend on current modelItem
386
- const { mainGraph } = this.getModel();
387
- if (mainGraph && mainGraph.hasNode(modelItem.path)) {
388
- const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
389
- // ### iterate dependant modelItems and refresh all their boundControls
390
- if (deps.length !== 0) {
391
- deps.forEach(dep => {
392
- // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
393
- const basePath = XPathUtil.getBasePath(dep);
394
- const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
395
- // ### refresh all boundControls
396
- modelItemOfDep.boundControls.forEach(control => {
397
- control.refresh();
398
- });
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),
399
489
  });
400
- needsRefresh = true;
401
- }
402
490
  }
403
- });
404
- this.toRefresh = [];
405
- if (!needsRefresh) {
406
- console.log('skipping refresh - no dependants');
407
- }
408
- } else {
409
- Fore.refreshChildren(this, true);
410
- console.timeEnd('refreshChildren');
411
491
  }
412
492
 
413
- // ### refresh template expressions
414
- if (this.initialRun || this.someInstanceDataStructureChanged) {
415
- this._updateTemplateExpressions();
416
- this.someInstanceDataStructureChanged = false; // reset
417
- }
418
- this._processTemplateExpressions();
419
-
420
- console.timeEnd('refresh');
421
-
422
- console.groupEnd();
423
- // console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
424
- this.dispatchEvent(new CustomEvent('refresh-done'));
425
- }
426
-
427
- /**
428
- * entry point for processing of template expression enclosed in '{}' brackets.
429
- *
430
- * Expressions are found with an XPath search. For each node an entry is added to the storedTemplateExpressionByNode map.
431
- *
432
- *
433
- * @private
434
- */
435
- _updateTemplateExpressions() {
436
- // Note the fact we're going over HTML here: therefore the `html` prefix.
437
- const search =
438
- "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::fx-model/descendant-or-self::node()/(., @*)";
439
-
440
- const tmplExpressions = evaluateXPathToNodes(search, this, this);
441
- // console.log('template expressions found ', tmplExpressions);
442
-
443
- if (!this.storedTemplateExpressions) {
444
- 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);
445
501
  }
446
502
 
447
- 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
+ });
448
533
 
449
- /*
450
- storing expressions and their nodes for re-evaluation
451
- */
452
- Array.from(tmplExpressions).forEach(node => {
453
- if (this.storedTemplateExpressionByNode.has(node)) {
454
- // If the node is already known, do not process it twice
455
- return;
456
- }
457
- const expr = this._getTemplateExpression(node);
458
-
459
- // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
460
- this.storedTemplateExpressionByNode.set(node, expr);
461
- });
462
- console.log('stored template expressions ', this.storedTemplateExpressionByNode);
463
-
464
- // TODO: Should we clean up nodes that existed but are now gone?
465
- this._processTemplateExpressions();
466
- }
467
-
468
- _processTemplateExpressions() {
469
- for (const node of this.storedTemplateExpressionByNode.keys()) {
470
- this._processTemplateExpression({
471
- node,
472
- expr: this.storedTemplateExpressionByNode.get(node),
473
- });
474
- }
475
- }
476
-
477
- // eslint-disable-next-line class-methods-use-this
478
- _processTemplateExpression(exprObj) {
479
- // console.log('processing template expression ', exprObj);
480
-
481
- const { expr } = exprObj;
482
- const { node } = exprObj;
483
- // console.log('expr ', expr);
484
- this.evaluateTemplateExpression(expr, node, this);
485
- }
486
-
487
- /**
488
- * evaluate a template expression (some expression in {} brackets) on a node (either text- or attribute node.
489
- * @param expr the XPath to evaluate
490
- * @param node the node which will get updated with evaluation result
491
- * @param form the form element
492
- */
493
- evaluateTemplateExpression(expr, node) {
494
- if (expr === '{}') return;
495
- const matches = expr.match(/{[^}]*}/g);
496
- if (matches) {
497
- matches.forEach(match => {
498
- // console.log('match ', match);
499
- let naked = match.substring(1, match.length - 1);
500
- const inscope = getInScopeContext(node, naked);
501
- if (!inscope) {
502
- const errNode =
503
- node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
504
- ? node.parentNode
505
- : node;
506
- console.warn('no inscope context for ', errNode);
507
- return;
508
- }
509
- // Templates are special: they use the namespace configuration from the place where they are
510
- // being defined
511
- const instanceId = XPathUtil.getInstanceId(naked);
512
- // console.log('target instance ', instanceId);
513
- const inst = this.getModel().getInstance(instanceId);
514
- try {
515
- const result = evaluateXPathToString(naked, inscope, node, null, inst);
516
-
517
- // console.log('result of eval ', result);
518
- const replaced = expr.replaceAll(match, result);
519
- // console.log('result of replacing ', replaced);
520
-
521
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
534
+ if (node.nodeType === Node.ATTRIBUTE_NODE) {
522
535
  const parent = node.ownerElement;
523
-
524
- // parent.setAttribute(name, replaced);
525
536
  parent.setAttribute(node.nodeName, replaced);
526
- } else if (node.nodeType === Node.TEXT_NODE) {
537
+ } else if (node.nodeType === Node.TEXT_NODE) {
527
538
  node.textContent = replaced;
528
- }
529
-
530
- if (replaced.includes('{')) {
531
- // console.log('need to go next round');
532
-
533
- // todo: duplicated code here - see above
534
- naked = replaced.substring(1, replaced.length);
535
- this.evaluateTemplateExpression(replaced, node);
536
- }
537
- } catch (error) {
538
- // console.log(error);
539
- this.dispatchEvent(new CustomEvent('error', { detail: error }));
540
539
  }
541
- });
542
540
  }
543
- }
544
541
 
545
- // eslint-disable-next-line class-methods-use-this
546
- _getTemplateExpression(node) {
547
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
548
- return node.value;
549
- }
550
- if (node.nodeType === Node.TEXT_NODE) {
551
- 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;
552
551
  }
553
- return null;
554
- }
555
-
556
- /**
557
- * called when `model-construct-done` event is received to
558
- * start initing of the UI.
559
- *
560
- * @private
561
- */
562
- _handleModelConstructDone() {
563
- this._initUI();
564
- }
565
-
566
- /**
567
- * If there's no instance element found in a fx-model during init it will construct
568
- * an instance from UI bindings.
569
- *
570
- * @returns {Promise<void>}
571
- * @private
572
- */
573
- async _lazyCreateInstance() {
574
- const model = this.querySelector('fx-model');
575
- if (model.instances.length === 0) {
576
- console.log('### lazy creation of instance');
577
- const generatedInstance = document.createElement('fx-instance');
578
- model.appendChild(generatedInstance);
579
-
580
- const generated = document.implementation.createDocument(null, 'data', null);
581
- // const newData = this._generateInstance(this, generated.firstElementChild);
582
- this._generateInstance(this, generated.firstElementChild);
583
- generatedInstance.instanceData = generated;
584
- model.instances.push(generatedInstance);
585
- 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();
586
561
  }
587
- }
588
-
589
- /**
590
- * @param {Element} start
591
- * @param {Element} parent
592
- */
593
- _generateInstance(start, parent) {
594
- if (start.hasAttribute('ref') && !Fore.isActionElement(start.nodeName)) {
595
- const ref = start.getAttribute('ref');
596
-
597
- if (ref.includes('/')) {
598
- console.log('complex path to create ', ref);
599
- const steps = ref.split('/');
600
- steps.forEach(step => {
601
- console.log('step ', step);
602
-
603
- // const generated = document.createElement(ref);
604
- parent = this._generateNode(parent, step, start);
605
- });
606
- } else {
607
- parent = this._generateNode(parent, ref, start);
608
- }
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
+ }
609
584
  }
610
585
 
611
- if (start.hasChildNodes()) {
612
- const list = start.children;
613
- for (let i = 0; i < list.length; i += 1) {
614
- this._generateInstance(list[i], parent);
615
- }
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;
616
615
  }
617
- return parent;
618
- }
619
-
620
- // eslint-disable-next-line class-methods-use-this
621
- _generateNode(parent, step, start) {
622
- const generated = parent.ownerDocument.createElement(step);
623
- if (start.children.length === 0) {
624
- 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;
625
626
  }
626
- parent.appendChild(generated);
627
- parent = generated;
628
- return parent;
629
- }
630
627
 
631
- /*
632
- _createStep(){
628
+ /*
629
+ _createStep(){
633
630
 
634
- }
635
- */
631
+ }
632
+ */
636
633
 
637
- /*
638
- _generateInstance(start, parent) {
639
- if (start.hasAttribute('ref')) {
640
- const ref = start.getAttribute('ref');
634
+ /*
635
+ _generateInstance(start, parent) {
636
+ if (start.hasAttribute('ref')) {
637
+ const ref = start.getAttribute('ref');
641
638
 
642
- if(ref.includes('/')){
643
- console.log('complex path to create ', ref);
644
- const steps = ref.split('/');
645
- steps.forEach(step => {
646
- 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);
647
644
 
648
645
 
649
- });
650
- }
646
+ });
647
+ }
651
648
 
652
- // const generated = document.createElement(ref);
653
- const generated = parent.ownerDocument.createElement(ref);
654
- if (start.children.length === 0) {
655
- 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;
656
656
  }
657
- parent.appendChild(generated);
658
- parent = generated;
659
- }
660
657
 
661
- if (start.hasChildNodes()) {
662
- const list = start.children;
663
- for (let i = 0; i < list.length; i += 1) {
664
- 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
+ }
665
663
  }
664
+ return parent;
666
665
  }
667
- 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);
668
720
  }
669
- */
670
-
671
- /**
672
- * Start the initialization of the UI by
673
- *
674
- * 1. checking if a instance needs to be generated
675
- * 2. attaching lazy loading intersection observers if `refresh-on-view` attributes are found
676
- * 3. doing a full refresh of the UI
677
- *
678
- * @returns {Promise<void>}
679
- * @private
680
- */
681
- async _initUI() {
682
- console.log('### _initUI()');
683
- if (!this.initialRun) return;
684
- await this._lazyCreateInstance();
685
-
686
- // console.log('registering variables!');
687
- const variables = new Map();
688
- (function registerVariables(node) {
689
- for (const child of node.children) {
690
- if ('setInScopeVariables' in child) {
691
- child.setInScopeVariables(variables);
721
+ }
722
+
723
+ unRegisterLazyElement(element) {
724
+ if (this.intersectionObserver) {
725
+ this.intersectionObserver.unobserve(element);
692
726
  }
693
- registerVariables(child);
694
- }
695
- })(this);
696
- console.log('Found variables:', variables);
727
+ }
697
728
 
698
- /*
699
- const options = {
700
- root: null,
701
- rootMargin: '0px',
702
- threshold: 0.3,
703
- };
704
- */
705
-
706
- await this.refresh();
707
- // this.style.display='block'
708
- this.classList.add('fx-ready');
709
- document.body.classList.add('fx-ready');
710
-
711
- this.ready = true;
712
- this.initialRun = false;
713
- console.log('### >>>>> dispatching ready >>>>>', this);
714
- console.log('modelItems: ', this.getModel().modelItems);
715
- console.log('### <<<<< FORE: form fully initialized...', this);
716
- Fore.dispatch(this, 'ready', {});
717
- }
718
-
719
- registerLazyElement(element) {
720
- if (this.intersectionObserver) {
721
- // console.log('registerLazyElement',element);
722
- this.intersectionObserver.observe(element);
729
+ /**
730
+ *
731
+ * @returns {FxModel}
732
+ */
733
+ getModel() {
734
+ return this.querySelector('fx-model');
723
735
  }
724
- }
725
736
 
726
- unRegisterLazyElement(element) {
727
- if (this.intersectionObserver) {
728
- this.intersectionObserver.unobserve(element);
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);
729
751
  }
730
- }
731
-
732
- /**
733
- *
734
- * @returns {FxModel}
735
- */
736
- getModel() {
737
- return this.querySelector('fx-model');
738
- }
739
-
740
- _displayMessage(e) {
741
- // console.log('_displayMessage',e);
742
- const { level } = e.detail;
743
- const msg = e.detail.message;
744
- this._showMessage(level, msg);
745
- e.stopPropagation();
746
- }
747
-
748
- _displayError(e) {
749
- // const { error } = e.detail;
750
- const msg = e.detail.message;
751
- // this._showMessage('modal', msg);
752
- const toast = this.shadowRoot.querySelector('#error');
753
- toast.showToast(msg);
754
- }
755
-
756
- _showMessage(level, msg) {
757
- if (level === 'modal') {
758
- // this.$.messageContent.innerText = msg;
759
- // this.$.modalMessage.open();
760
-
761
- this.shadowRoot.getElementById('messageContent').innerText = msg;
762
- // this.shadowRoot.getElementById('modalMessage').open();
763
- this.shadowRoot.getElementById('modalMessage').classList.add('show');
764
- } else if (level === 'sticky') {
765
- // const notification = this.$.modeless;
766
- this.shadowRoot.querySelector('#sticky').showToast(msg);
767
- } else {
768
- const toast = this.shadowRoot.querySelector('#message');
769
- toast.showToast(msg);
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
+ }
770
768
  }
771
- }
772
769
  }
773
770
 
774
771
  if (!customElements.get('fx-fore')) {
775
- customElements.define('fx-fore', FxFore);
772
+ customElements.define('fx-fore', FxFore);
776
773
  }