@jinntec/fore 1.0.0-1 → 1.0.0-4

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 (43) hide show
  1. package/README.md +20 -1
  2. package/dist/fore-all.js +10 -10
  3. package/dist/fore-debug.js +140 -0
  4. package/index.js +8 -0
  5. package/package.json +7 -6
  6. package/resources/{vars.css → fore-styles.css} +292 -0
  7. package/src/DependencyNotifyingDomFacade.js +9 -1
  8. package/src/ForeElementMixin.js +25 -17
  9. package/src/actions/abstract-action.js +8 -7
  10. package/src/actions/fx-action.js +6 -1
  11. package/src/actions/fx-dispatch.js +10 -2
  12. package/src/actions/fx-hide.js +23 -0
  13. package/src/actions/fx-insert.js +9 -9
  14. package/src/actions/fx-send.js +1 -1
  15. package/src/actions/fx-setvalue.js +2 -4
  16. package/src/actions/fx-show.js +23 -0
  17. package/src/dep_graph.js +9 -0
  18. package/src/drawdown.js +172 -0
  19. package/src/fore.js +56 -3
  20. package/src/fx-bind.js +20 -17
  21. package/src/fx-fore.js +297 -33
  22. package/src/fx-instance.js +27 -6
  23. package/src/fx-model.js +182 -37
  24. package/src/fx-submission.js +29 -7
  25. package/src/fx-var.js +43 -0
  26. package/src/getInScopeContext.js +28 -7
  27. package/src/modelitem.js +1 -0
  28. package/src/ui/abstract-control.js +17 -4
  29. package/src/ui/fx-alert.js +16 -20
  30. package/src/ui/fx-container.js +9 -4
  31. package/src/ui/fx-control.js +76 -39
  32. package/src/ui/fx-dialog.js +68 -0
  33. package/src/ui/fx-inspector.js +44 -0
  34. package/src/ui/fx-items.js +120 -0
  35. package/src/ui/fx-output.js +50 -12
  36. package/src/ui/fx-repeat.js +83 -16
  37. package/src/ui/fx-repeatitem.js +11 -4
  38. package/src/ui/fx-trigger.js +3 -0
  39. package/src/xpath-evaluation.js +372 -135
  40. package/src/xpath-util.js +52 -12
  41. package/dist/fore.js +0 -2
  42. package/resources/fore.css +0 -86
  43. package/resources/toastify.css +0 -87
package/src/fx-fore.js CHANGED
@@ -4,24 +4,34 @@ import './fx-model.js';
4
4
  import '@jinntec/jinn-toast';
5
5
  import { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
6
6
  import getInScopeContext from './getInScopeContext.js';
7
+ import { XPathUtil } from './xpath-util.js';
7
8
 
8
9
  /**
9
10
  * Main class for Fore.Outermost container element for each Fore application.
10
11
  *
11
- *
12
- *
13
12
  * Root element for Fore. Kicks off initialization and displays messages.
14
13
  *
15
14
  * fx-fore is the outermost container for each form. A form can have exactly one model
16
15
  * with arbitrary number of instances.
17
16
  *
18
- * Main responsiblities are initialization of model, update of UI (refresh) and global messaging
17
+ * Main responsiblities are initialization and updating of model and instances, update of UI (refresh) and global messaging.
18
+ *
19
+ *
19
20
  *
20
21
  * @ts-check
21
22
  */
22
23
  export class FxFore extends HTMLElement {
23
24
  static get properties() {
24
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
+ },
25
35
  model: {
26
36
  type: Object,
27
37
  },
@@ -31,6 +41,14 @@ export class FxFore extends HTMLElement {
31
41
  };
32
42
  }
33
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
+ */
34
52
  constructor() {
35
53
  super();
36
54
  this.model = {};
@@ -42,12 +60,11 @@ export class FxFore extends HTMLElement {
42
60
  });
43
61
 
44
62
  this.ready = false;
45
-
46
63
  this.storedTemplateExpressionByNode = new Map();
47
64
 
48
65
  const style = `
49
66
  :host {
50
- display: block;
67
+ // display: none;
51
68
  height:auto;
52
69
  padding:var(--model-element-padding);
53
70
  font-family:Roboto, sans-serif;
@@ -56,6 +73,11 @@ export class FxFore extends HTMLElement {
56
73
  :host ::slotted(fx-model){
57
74
  display:none;
58
75
  }
76
+ :host(.fx-ready){
77
+ animation: fadein .4s forwards;
78
+ display:block;
79
+ }
80
+
59
81
  #modalMessage .dialogActions{
60
82
  text-align:center;
61
83
  }
@@ -118,6 +140,14 @@ export class FxFore extends HTMLElement {
118
140
  #messageContent{
119
141
  margin-top:40px;
120
142
  }
143
+ @keyframes fadein {
144
+ 0% {
145
+ opacity:0;
146
+ }
147
+ 100% {
148
+ opacity:1;
149
+ }
150
+ }
121
151
  `;
122
152
 
123
153
  const html = `
@@ -140,9 +170,29 @@ export class FxFore extends HTMLElement {
140
170
  </style>
141
171
  ${html}
142
172
  `;
173
+
174
+ this.toRefresh = [];
175
+ this.initialRun = true;
176
+ this.someInstanceDataStructureChanged = false;
143
177
  }
144
178
 
145
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);
188
+ }
189
+
190
+ this.src = this.hasAttribute('src')? this.getAttribute('src'):null;
191
+ if(this.src){
192
+ this._loadFromSrc();
193
+ return ;
194
+ }
195
+
146
196
  const slot = this.shadowRoot.querySelector('slot');
147
197
  slot.addEventListener('slotchange', event => {
148
198
  const children = event.target.assignedElements();
@@ -155,18 +205,123 @@ export class FxFore extends HTMLElement {
155
205
  modelElement = generatedModel;
156
206
  }
157
207
  if (!modelElement.inited) {
158
- console.log('########## FORE: kick off processing... ##########');
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, '##########');
213
+ }
159
214
  modelElement.modelConstruct();
160
215
  }
161
216
  this.model = modelElement;
162
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
+ }
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
+
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);
272
+ })
273
+ .catch(error => {
274
+ this.dispatchEvent(new CustomEvent('error',{detail:{message: `'${this.src}' not found or does not contain Fore element.`}}));
275
+ });
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);
301
+ } else {
302
+ console.log('refreshing children', target);
303
+ Fore.refreshChildren(target, true);
304
+ }
305
+ }
306
+ });
307
+ entries[0].target.getOwnerForm().dispatchEvent(new CustomEvent('refresh-done'));
308
+
309
+ console.timeEnd('refreshLazy');
163
310
  }
164
311
 
165
312
  evaluateToNodes(xpath, context) {
166
313
  return evaluateXPathToNodes(xpath, context, this);
167
314
  }
168
315
 
169
- disconnectedCallback() {}
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
+ }
170
325
 
171
326
  /**
172
327
  * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
@@ -176,16 +331,63 @@ export class FxFore extends HTMLElement {
176
331
  * AVT:
177
332
  *
178
333
  */
179
- async refresh() {
334
+ async refresh(force) {
180
335
  // refresh () {
181
336
  console.group('### refresh');
182
- // await this.updateComplete;
183
337
 
184
- Fore.refreshChildren(this);
185
- // this.dispatchEvent(new CustomEvent('refresh-done', {detail:'foo'}));
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
+ });
355
+ }
356
+
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()});
369
+ });
370
+ needsRefresh = true;
371
+ }
372
+ }
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
+ }
186
382
 
187
383
  // ### refresh template expressions
188
- this._updateTemplateExpressions();
384
+ if(this.initialRun || this.someInstanceDataStructureChanged){
385
+ this._updateTemplateExpressions();
386
+ this.someInstanceDataStructureChanged = false; //reset
387
+ }
388
+ this._processTemplateExpressions();
389
+
390
+ console.timeEnd('refresh');
189
391
 
190
392
  console.groupEnd();
191
393
  console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
@@ -203,14 +405,20 @@ export class FxFore extends HTMLElement {
203
405
  _updateTemplateExpressions() {
204
406
  // Note the fact we're going over HTML here: therefore the `html` prefix.
205
407
  const search =
206
- "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::xhtml:fx-model/descendant-or-self::node()/(., @*)";
408
+ "(descendant-or-self::*/(text(), @*))[matches(.,'\\{.*\\}')] except descendant-or-self::fx-model/descendant-or-self::node()/(., @*)";
207
409
 
208
410
  const tmplExpressions = evaluateXPathToNodes(search, this, this);
209
411
  console.log('template expressions found ', tmplExpressions);
210
412
 
413
+ if (!this.storedTemplateExpressions) {
414
+ this.storedTemplateExpressions = [];
415
+ }
416
+
417
+ console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
418
+
211
419
  /*
212
- storing expressions and their nodes for re-evaluation
213
- */
420
+ storing expressions and their nodes for re-evaluation
421
+ */
214
422
  Array.from(tmplExpressions).forEach(node => {
215
423
  if (this.storedTemplateExpressionByNode.has(node)) {
216
424
  // If the node is already known, do not process it twice
@@ -218,21 +426,26 @@ export class FxFore extends HTMLElement {
218
426
  }
219
427
  const expr = this._getTemplateExpression(node);
220
428
 
429
+ // console.log('storedTemplateExpressionByNode', this.storedTemplateExpressionByNode);
221
430
  this.storedTemplateExpressionByNode.set(node, expr);
222
431
  });
432
+ console.log('stored template expressions ', this.storedTemplateExpressionByNode);
223
433
 
224
434
  // TODO: Should we clean up nodes that existed but are now gone?
435
+ this._processTemplateExpressions();
436
+
437
+ }
438
+
439
+ _processTemplateExpressions() {
225
440
  for (const node of this.storedTemplateExpressionByNode.keys()) {
226
441
  this._processTemplateExpression({
227
442
  node,
228
443
  expr: this.storedTemplateExpressionByNode.get(node),
229
444
  });
230
445
  }
231
-
232
- console.log('stored template expressions ', this.storedTemplateExpressionByNode);
233
446
  }
234
447
 
235
- // eslint-disable-next-line class-methods-use-this
448
+ // eslint-disable-next-line class-methods-use-this
236
449
  _processTemplateExpression(exprObj) {
237
450
  // console.log('processing template expression ', exprObj);
238
451
 
@@ -268,13 +481,15 @@ export class FxFore extends HTMLElement {
268
481
  }
269
482
  // Templates are special: they use the namespace configuration from the place where they are
270
483
  // being defined
271
-
484
+ const instanceId = XPathUtil.getInstanceId(naked);
485
+ // console.log('target instance ', instanceId);
486
+ const inst = this.getModel().getInstance(instanceId);
272
487
  try {
273
- const result = evaluateXPathToString(naked, inscope, node, null, namespaceContextNode);
488
+ const result = evaluateXPathToString(naked, inscope, node, null, inst);
274
489
 
275
490
  // console.log('result of eval ', result);
276
491
  const replaced = expr.replaceAll(match, result);
277
- console.log('result of replacing ', replaced);
492
+ // console.log('result of replacing ', replaced);
278
493
 
279
494
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
280
495
  const parent = node.ownerElement;
@@ -286,7 +501,7 @@ export class FxFore extends HTMLElement {
286
501
  }
287
502
 
288
503
  if (replaced.includes('{')) {
289
- console.log('need to go next round');
504
+ // console.log('need to go next round');
290
505
 
291
506
  // todo: duplicated code here - see above
292
507
  naked = replaced.substring(1, replaced.length);
@@ -305,25 +520,28 @@ export class FxFore extends HTMLElement {
305
520
  return node.value;
306
521
  }
307
522
  if (node.nodeType === Node.TEXT_NODE) {
308
- return node.textContent;
523
+ return node.textContent.trim();
309
524
  }
310
525
  return null;
311
526
  }
312
527
 
313
- _refreshChildren() {
314
- const uiElements = this.querySelectorAll('*');
315
-
316
- uiElements.forEach(element => {
317
- if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
318
- element.refresh();
319
- }
320
- });
321
- }
322
-
528
+ /**
529
+ * called when `model-construct-done` event is received to
530
+ * start initing of the UI.
531
+ *
532
+ * @private
533
+ */
323
534
  _handleModelConstructDone() {
324
535
  this._initUI();
325
536
  }
326
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
+ */
327
545
  async _lazyCreateInstance() {
328
546
  const model = this.querySelector('fx-model');
329
547
  if (model.instances.length === 0) {
@@ -422,18 +640,64 @@ export class FxFore extends HTMLElement {
422
640
  }
423
641
  */
424
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
+ */
425
653
  async _initUI() {
426
654
  console.log('### _initUI()');
427
655
 
428
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);
664
+ }
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
+
429
676
  await this.refresh();
677
+ // this.style.display='block'
678
+ this.classList.add('fx-ready');
679
+
430
680
  this.ready = true;
681
+ this.initialRun = false;
431
682
  console.log('### <<<<< dispatching ready >>>>>');
432
683
  console.log('########## modelItems: ', this.getModel().modelItems);
433
684
  console.log('########## FORE: form fully initialized... ##########');
434
685
  this.dispatchEvent(new CustomEvent('ready', {}));
435
686
  }
436
687
 
688
+ registerLazyElement(element) {
689
+ if (this.intersectionObserver) {
690
+ // console.log('registerLazyElement',element);
691
+ this.intersectionObserver.observe(element);
692
+ }
693
+ }
694
+
695
+ unRegisterLazyElement(element) {
696
+ if (this.intersectionObserver) {
697
+ this.intersectionObserver.unobserve(element);
698
+ }
699
+ }
700
+
437
701
  /**
438
702
  *
439
703
  * @returns {FxModel}
@@ -144,6 +144,7 @@ export class FxInstance extends HTMLElement {
144
144
 
145
145
  _createInstanceData() {
146
146
  if (this.type === 'xml') {
147
+ // const doc = new DOMParser().parseFromString('<data data-id="default"></data>', 'application/xml');
147
148
  const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
148
149
  this.instanceData = doc;
149
150
  }
@@ -167,7 +168,19 @@ export class FxInstance extends HTMLElement {
167
168
  .then(response => {
168
169
  const responseContentType = response.headers.get('content-type').toLowerCase();
169
170
  console.log('********** responseContentType *********', responseContentType);
170
- if (responseContentType.startsWith('text/plain')) {
171
+ if (responseContentType.startsWith('text/html')) {
172
+ // const htmlResponse = response.text();
173
+ // return new DOMParser().parseFromString(htmlResponse, 'text/html');
174
+ // return response.text();
175
+ return response.text().then(result =>
176
+ // console.log('xml ********', result);
177
+ new DOMParser().parseFromString(result, 'text/html'),
178
+ );
179
+ }
180
+ if (
181
+ responseContentType.startsWith('text/plain') ||
182
+ responseContentType.startsWith('text/markdown')
183
+ ) {
171
184
  // console.log("********** inside res plain *********");
172
185
  return response.text();
173
186
  }
@@ -176,16 +189,20 @@ export class FxInstance extends HTMLElement {
176
189
  return response.json();
177
190
  }
178
191
  if (responseContentType.startsWith('application/xml')) {
179
- return response.text().then(result => {
180
- console.log('xml ********', result);
181
- return new DOMParser().parseFromString(result, 'application/xml');
182
- });
192
+ return response.text().then(result =>
193
+ // console.log('xml ********', result);
194
+ new DOMParser().parseFromString(result, 'application/xml'),
195
+ );
183
196
  }
184
197
  return 'done';
185
198
  })
186
199
  .then(data => {
200
+ if (data.nodeType) {
201
+ this.instanceData = data;
202
+ console.log('instanceData loaded: ', this.instanceData);
203
+ return;
204
+ }
187
205
  this.instanceData = data;
188
- console.log('instanceData loaded: ', this.instanceData);
189
206
  })
190
207
  .catch(error => {
191
208
  throw new Error(`failed loading data ${error}`);
@@ -218,6 +235,10 @@ export class FxInstance extends HTMLElement {
218
235
  // todo: move innerHTML out to shadowDOM (for later reset)
219
236
  } else if (this.type === 'json') {
220
237
  this.instanceData = JSON.parse(this.textContent);
238
+ } else if (this.type === 'html') {
239
+ this.instanceData = this.firstElementChild.children;
240
+ } else if (this.type === 'text') {
241
+ this.instanceData = this.textContent;
221
242
  } else {
222
243
  console.warn('unknow type for data ', this.type);
223
244
  }