@jinntec/fore 1.10.3 → 2.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/index.js CHANGED
@@ -22,6 +22,7 @@ import './src/ui/fx-case.js';
22
22
  import './src/ui/fx-inspector.js';
23
23
  import './src/ui/fx-dialog.js';
24
24
  import './src/ui/fx-items.js';
25
+ import './src/ui/fx-droptarget.js';
25
26
 
26
27
  import './src/tools/fx-action-log.js';
27
28
  import './src/tools/fx-log-settings.js';
@@ -57,6 +58,8 @@ import './src/actions/fx-reset.js';
57
58
  import './src/actions/fx-load.js';
58
59
  import './src/actions/fx-toggleboolean.js';
59
60
  import './src/actions/fx-call.js';
61
+ import './src/actions/fx-setattribute.js';
62
+ import './src/actions/fx-construct-done.js';
60
63
 
61
64
  import './src/functions/fx-function.js';
62
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "1.10.3",
3
+ "version": "2.1.0",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
@@ -1,5 +1,5 @@
1
1
  import {foreElementMixin} from '../ForeElementMixin.js';
2
- import {evaluateXPathToBoolean, resolveId} from '../xpath-evaluation.js';
2
+ import {evaluateXPathToBoolean, resolveId, evaluateXPath} from '../xpath-evaluation.js';
3
3
  import getInScopeContext from '../getInScopeContext.js';
4
4
  import {Fore} from '../fore.js';
5
5
  import {FxFore} from '../fx-fore.js';
@@ -56,6 +56,18 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
56
56
  ifExpr: {
57
57
  type: String,
58
58
  },
59
+ /**
60
+ * The iterate attribute can be added to any XForms action. It contains an expression
61
+ * that is evaluated once using the in-scope evaluation context before the action is
62
+ * executed, which will result in a sequence of items. The action will be executed with
63
+ * each item in the sequence as its context. This context replaces the default in scope
64
+ * evaluation context.
65
+ *
66
+ * The interaction with `@while` and `@if` is undefined.
67
+ */
68
+ iterateExpr: {
69
+ type: String
70
+ },
59
71
  /**
60
72
  * whether nor not an action needs to run the update cycle
61
73
  */
@@ -88,8 +100,8 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
88
100
  type: String,
89
101
  },
90
102
  /**
91
- * boolean XPath expression. If true loop will be executed. If an ifExpr is present this also needs to be true
92
- * to actually run the action.
103
+ * boolean XPath expression. If true loop will be executed. If an ifExpr is present this
104
+ * also needs to be true to actually run the action.
93
105
  */
94
106
  whileExpr: {
95
107
  type: String,
@@ -103,6 +115,9 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
103
115
  this.needsUpdate = false;
104
116
  }
105
117
 
118
+ disconnectedCallback() {
119
+ }
120
+
106
121
  connectedCallback() {
107
122
  this.style.display = 'none';
108
123
  this.propagate = this.hasAttribute('propagate') ? this.getAttribute('propagate') : 'continue';
@@ -132,6 +147,7 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
132
147
  this.ifExpr = this.hasAttribute('if') ? this.getAttribute('if') : null;
133
148
  this.whileExpr = this.hasAttribute('while') ? this.getAttribute('while') : null;
134
149
  this.delay = this.hasAttribute('delay') ? Number(this.getAttribute('delay')) : 0;
150
+ this.iterateExpr = this.hasAttribute('iterate') ? this.getAttribute('iterate') : null;
135
151
 
136
152
  this._addUpdateListener();
137
153
 
@@ -185,9 +201,7 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
185
201
  * @param e
186
202
  */
187
203
  async execute(e) {
188
- console.log(this);
189
- // console.log('execute', this.event);
190
-
204
+ console.log(this, this.event);
191
205
 
192
206
  if (e && e.target.nodeType !== Node.DOCUMENT_NODE && e.target !== window ){
193
207
  /*
@@ -219,19 +233,8 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
219
233
  );
220
234
  }
221
235
 
222
- // console.log('executing', this);
223
- // console.log('executing e', e);
224
- // console.log('executing e phase', e.eventPhase);
236
+ // Outermost handling
225
237
  if (FxFore.outermostHandler === null) {
226
- // console.time('outermostHandler');
227
- /*
228
- console.info(
229
- `%coutermost Action `,
230
- 'background:#e65100; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
231
- this,
232
- );
233
- */
234
- // console.log('starting outermost handler',this);
235
238
  FxFore.outermostHandler = this;
236
239
  this.dispatchEvent(new CustomEvent('outermost-action-start', {
237
240
  composed: true,
@@ -240,16 +243,6 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
240
243
  }));
241
244
  }
242
245
 
243
- /*
244
- if (FxFore.outermostHandler !== this) {
245
- console.info(
246
- `%cAction `,
247
- 'background:orange; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
248
- this,
249
- );
250
- }
251
- */
252
- // console.log('>>> outermostHandler', FxFore.outermostHandler);
253
246
 
254
247
  if (e) {
255
248
  this.currentEvent = e;
@@ -265,51 +258,26 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
265
258
  this.nodeset = this.targetElement.nodeset;
266
259
  }
267
260
 
268
- // First check if 'if' condition is true - otherwise exist right away
261
+ // Order of application between if / while and iterate is undefined. See
262
+ // https://www.w3.org/MarkUp/Forms/wiki/@iterate
263
+ if (this.iterateExpr) {
264
+ // Same as whileExpr, let it go update UI afterwards
265
+ await this.handleIterateExpr();
266
+ this._finalizePerform(resolveThisEvent);
267
+ return;
268
+ }
269
+
270
+ // Check if 'if' condition is true - otherwise exist right away
269
271
  if (this.ifExpr && !evaluateXPathToBoolean(this.ifExpr, getInScopeContext(this), this)) {
270
272
  this._finalizePerform(resolveThisEvent);
271
273
  return;
272
274
  }
273
275
 
274
- // console.log('performing action', this);
275
-
276
276
  if (this.whileExpr) {
277
- // While: while the condition is true, delay a bit and execute the action
278
- const loop = async () => {
279
- // Start by waiting
280
- await wait(this.delay || 0);
281
-
282
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
283
- // We are no longer in the document. Stop working
284
- return;
285
- }
286
-
287
- if (!evaluateXPathToBoolean(this.whileExpr, getInScopeContext(this), this)) {
288
- // Done with iterating
289
- return;
290
- }
291
-
292
- // Perform the action once. But quit if it errored
293
- if (!this.performSafe()) {
294
- return;
295
- }
296
-
297
- // Go for one more iteration
298
- if (this.delay) {
299
- // If we have a delay, fire and forget this.
300
- // Otherwise, if we have no delay, keep waiting for all iterations to be done.
301
- // The while is then uninterruptable and immediate
302
- loop();
303
- return;
304
- }
305
- await loop();
306
- };
307
-
308
- // After loop is done call actionPerformed to update the model and UI
309
- await loop();
310
- this._finalizePerform(resolveThisEvent);
311
-
312
- return;
277
+ // After loop is done call actionPerformed to update the model and UI
278
+ await this.handleWhileExpr();
279
+ this._finalizePerform(resolveThisEvent);
280
+ return;
313
281
  }
314
282
 
315
283
  if (this.delay) {
@@ -327,6 +295,72 @@ export class AbstractAction extends foreElementMixin(HTMLElement) {
327
295
  this._finalizePerform(resolveThisEvent);
328
296
  }
329
297
 
298
+ async handleWhileExpr () {
299
+ // While: while the condition is true, delay a bit and execute the action
300
+ // Start by waiting
301
+ await wait(this.delay || 0);
302
+
303
+ if (!XPathUtil.contains(this.getOwnerForm(), this)) {
304
+ // We are no longer in the document. Stop working
305
+ return;
306
+ }
307
+
308
+ if (!evaluateXPathToBoolean(this.whileExpr, getInScopeContext(this), this)) {
309
+ // Done with iterating
310
+ return;
311
+ }
312
+
313
+ // Perform the action once. But quit if it failed
314
+ if (!this.performSafe()) {
315
+ return;
316
+ }
317
+
318
+ // Go for one more iteration
319
+ if (this.delay) {
320
+ // If we have a delay, fire and forget this.
321
+ // Otherwise, if we have no delay, keep waiting for all iterations to be done.
322
+ // The while is then uninterruptable and immediate
323
+
324
+ this.handleWhileExpr();
325
+ return;
326
+ }
327
+
328
+
329
+ await this.handleWhileExpr();
330
+ }
331
+
332
+ async handleIterateExpr () {
333
+ try {
334
+ // Iterate: get the context sequence and perform the action once per item.
335
+ const contextSequence = evaluateXPath(this.iterateExpr, getInScopeContext(this), this);
336
+
337
+ if (contextSequence.length === 0) {
338
+ return;
339
+ }
340
+
341
+ if (!XPathUtil.contains(this.getOwnerForm(), this)) {
342
+ // We are no longer in the document. Stop working
343
+ return;
344
+ }
345
+
346
+ for (const item of contextSequence) {
347
+ if (this.delay) {
348
+ await wait(this.delay || 0);
349
+ }
350
+
351
+ // This will be picked up in `getInscopeContext`
352
+ this.currentContext = item;
353
+
354
+ // Perform the action once. But quit if it failed
355
+ if (!await this.performSafe()) {
356
+ return;
357
+ }
358
+ }
359
+ } finally {
360
+ this.currentContext = null;
361
+ }
362
+ }
363
+
330
364
  _finalizePerform(resolveThisEvent) {
331
365
  this.currentEvent = null;
332
366
  this.actionPerformed();
@@ -18,15 +18,15 @@ export class FxConfirm extends FxAction {
18
18
  }
19
19
 
20
20
  connectedCallback() {
21
- if(super.connectedCallback){
22
- super.connectedCallback();
21
+ if (super.connectedCallback){
22
+ super.connectedCallback();
23
23
  }
24
24
  this.message = this.hasAttribute('message') ? this.getAttribute('message') : null;
25
25
  }
26
26
 
27
27
  async perform() {
28
28
  if (window.confirm(this.message)) {
29
- super.perform();
29
+ await super.perform();
30
30
  }
31
31
  }
32
32
  }
@@ -0,0 +1,28 @@
1
+ import { AbstractAction } from './abstract-action.js';
2
+ import {FxAction} from "./fx-action.js";
3
+ import {Fore} from "../fore.js";
4
+
5
+ /**
6
+ * `fx-action`
7
+ * an action to wrap other actions and defers the update cycle until the end of the block.
8
+ *
9
+ * @customElement
10
+ * @demo demo/index.html
11
+ */
12
+ export class FxConstructDone extends FxAction {
13
+ connectedCallback() {
14
+ // eslint-disable-next-line wc/guard-super-call
15
+ super.connectedCallback();
16
+ console.log('parentNode', this.parentNode);
17
+ if(this.parentNode.nodeName !== 'FX-MODEL'){
18
+ Fore.dispatch(this, 'error', {message:'parent is not a model'})
19
+ return;
20
+ }
21
+ this.parentNode.addEventListener('model-construct-done', e => {
22
+ super.perform();
23
+ });
24
+ }
25
+ }
26
+ if (!customElements.get('fx-construct-done')) {
27
+ window.customElements.define('fx-construct-done', FxConstructDone);
28
+ }
@@ -15,23 +15,23 @@ import {Fore} from '../fore.js';
15
15
  * @customElement
16
16
  */
17
17
  export class FxInsert extends AbstractAction {
18
- static get properties() {
19
- return {
20
- ...super.properties,
21
- at: {
22
- type: Number,
23
- },
24
- position: {
25
- type: Number,
26
- },
27
- origin: {
28
- type: Object,
29
- },
30
- keepValues: {
31
- type: Boolean,
32
- },
33
- };
34
- }
18
+ static get properties() {
19
+ return {
20
+ ...super.properties,
21
+ at: {
22
+ type: Number,
23
+ },
24
+ position: {
25
+ type: Number,
26
+ },
27
+ origin: {
28
+ type: Object,
29
+ },
30
+ keepValues: {
31
+ type: Boolean,
32
+ },
33
+ };
34
+ }
35
35
 
36
36
  constructor() {
37
37
  super();
@@ -127,26 +127,23 @@ export class FxInsert extends AbstractAction {
127
127
  }
128
128
 
129
129
  async perform() {
130
- // super.perform();
131
- // as we're overwriting the superclass we need to dispatch the execute-action ourselves
132
-
133
-
134
- /*
135
- todo: !!! calling super here does not correctly give the nodeset - it's likely still a bug in ForeElementMixin !!!
136
- // super.perform();
137
- console.log('this.nodeset', this.nodeset);
138
- */
139
-
130
+ // We have a few terms here: `inScope` is the 'current item' we have. It is the item we're
131
+ // copying and inserting elsewhere. If we have a `ref`, one of the nodes returned will
132
+ // become the sibling of this copy. The `context` is the new parent of the copied
133
+ // element. It's usually better to add a `context` because that deals with empty elements.
140
134
  let inscope;
135
+ let context;
136
+ let targetSequence = [];
137
+ const inscopeContext = getInScopeContext(this);
138
+
141
139
  // ### 'context' attribute takes precedence over 'ref'
142
- let targetSequence;
143
140
  if (this.hasAttribute('context')) {
144
- inscope = getInScopeContext(this.getAttributeNode('context'), this.getAttribute('context'));
145
- targetSequence = evaluateXPathToNodes(
141
+ [context] = evaluateXPathToNodes(
146
142
  this.getAttribute('context'),
147
- inscope,
143
+ inscopeContext,
148
144
  this.getOwnerForm(),
149
145
  );
146
+ inscope = inscopeContext;
150
147
  }
151
148
 
152
149
  if (this.hasAttribute('ref')) {
@@ -163,14 +160,19 @@ export class FxInsert extends AbstractAction {
163
160
  let insertLocationNode;
164
161
  let index;
165
162
 
166
- // const idx = this._getInsertIndex(inscope, targetSequence);
167
- // console.log('insert index', idx);
168
-
169
163
  // if the targetSequence is empty but we got an originSequence use inscope as context and ignore 'at' and 'position'
170
164
  if (targetSequence.length === 0) {
171
- insertLocationNode = inscope;
172
- inscope.appendChild(originSequenceClone);
173
- index = 1;
165
+ if (context) {
166
+ insertLocationNode = context;
167
+ context.appendChild(originSequenceClone);
168
+ index = 1;
169
+ } else {
170
+ // No context. We can insert into the `inscope`.
171
+ insertLocationNode = inscope;
172
+ inscope.appendChild(originSequenceClone);
173
+ index = 1;
174
+
175
+ }
174
176
  } else {
175
177
  /* ### insert at position given by 'at' or use the last item in the targetSequence ### */
176
178
  if (this.hasAttribute('at')) {
@@ -221,31 +223,31 @@ export class FxInsert extends AbstractAction {
221
223
  }
222
224
  }
223
225
  }
224
- // instance('default')/items/item[index()]
226
+ // instance('default')/items/item[index()]
225
227
 
226
228
  // console.log('insert context item ', insertLocationNode);
227
229
  // console.log('parent ', insertLocationNode.parentNode);
228
230
  // console.log('instance ', this.getModel().getDefaultContext());
229
231
  // Fore.dispatch()
230
232
 
231
- // const instanceId = XPathUtil.resolveInstance(this, this.getAttribute('context'));
233
+ // const instanceId = XPathUtil.resolveInstance(this, this.getAttribute('context'));
232
234
  const instanceId = XPathUtil.resolveInstance(this, this.ref);
233
235
  const inst = this.getModel().getInstance(instanceId);
234
- // console.log('<<<<<<< resolved instance', inst);
235
- // Note: the parent to insert under is always the parent of the inserted node. The 'context' is not always the parent if the sequence is empty, or the position is different
236
- // const xpath = XPathUtil.getPath(originSequenceClone.parentNode, instanceId);
236
+ // console.log('<<<<<<< resolved instance', inst);
237
+ // Note: the parent to insert under is always the parent of the inserted node. The 'context' is not always the parent if the sequence is empty, or the position is different
238
+ // const xpath = XPathUtil.getPath(originSequenceClone.parentNode, instanceId);
237
239
  const xpath = XPathUtil.getPath(insertLocationNode.parentNode, instanceId);
238
240
 
239
241
 
240
242
 
241
243
  const path = Fore.getDomNodeIndexString(originSequenceClone);
242
244
  this.dispatchEvent(
243
- new CustomEvent('execute-action', {
244
- composed: true,
245
- bubbles: true,
246
- cancelable:true,
247
- detail: { action: this, event:this.event, path },
248
- }),
245
+ new CustomEvent('execute-action', {
246
+ composed: true,
247
+ bubbles: true,
248
+ cancelable:true,
249
+ detail: { action: this, event:this.event, path },
250
+ }),
249
251
  );
250
252
 
251
253
  Fore.dispatch(inst,'insert',{
@@ -267,9 +269,9 @@ export class FxInsert extends AbstractAction {
267
269
  }),
268
270
  );
269
271
 
270
- this.needsUpdate = true;
271
- console.log('Changed!', xpath)
272
- return [xpath];
272
+ this.needsUpdate = true;
273
+ console.log('Changed!', xpath)
274
+ return [xpath];
273
275
  }
274
276
 
275
277
  // eslint-disable-next-line class-methods-use-this
@@ -5,10 +5,9 @@ import {XPathUtil} from "../xpath-util.js";
5
5
  import {Fore} from "../fore.js";
6
6
 
7
7
  /**
8
- * `fx-message`
9
- *
10
- * Action to display messages to the user.
8
+ * `fx-load`
11
9
  *
10
+ * Action to load a window, tab or embed some Html into the current page at given location.
12
11
  *
13
12
  */
14
13
  class FxLoad extends AbstractAction {
@@ -145,24 +144,14 @@ class FxLoad extends AbstractAction {
145
144
  });
146
145
  const data = await response.text();
147
146
  // console.log('data loaded: ', data);
147
+ // const data = Fore.loadHtml(resolvedUrl);
148
148
 
149
149
  // todo: if data contain '<template' element as first child instanciate and insert it
150
150
  if (!this.attachTo) {
151
151
  this.innerHtml = data;
152
152
  }
153
- /*
154
- if (this.attachTo.startsWith('#')) {
155
- const targetId = this.attachTo.substring(1);
156
- const resolved = resolveId(targetId, this);
157
- resolved.innerHTML = '';
158
- resolved.innerHTML = data;
159
- }
160
- */
161
153
  this._attachToElement(data);
162
-
163
-
164
154
  Fore.dispatch(this, 'loaded', {url: this.url})
165
-
166
155
  } catch (error) {
167
156
  throw new Error(`failed loading data ${error}`);
168
157
  }
@@ -228,27 +217,6 @@ class FxLoad extends AbstractAction {
228
217
  return replaced;
229
218
  }
230
219
 
231
-
232
- /*
233
- _getValue() {
234
- if (this.hasAttribute('value')) {
235
- const valAttr = this.getAttribute('value');
236
- try {
237
- const inscopeContext = getInScopeContext(this, valAttr);
238
- return evaluateXPathToString(valAttr, inscopeContext, this);
239
- } catch (error) {
240
- console.error(error);
241
- Fore.dispatch(this, 'error', {message: error});
242
- }
243
- }
244
- if (this.textContent) {
245
- return this.textContent;
246
- }
247
- return null;
248
- }
249
- */
250
-
251
-
252
220
  }
253
221
 
254
222
  if (!customElements.get('fx-load')) {
@@ -21,6 +21,7 @@ class FxRefresh extends AbstractAction {
21
21
  );
22
22
 
23
23
  if (this.hasAttribute('self')) {
24
+ console.log(`### <<<<< refresh() self ${this} >>>>>`);
24
25
  const control = XPathUtil.getClosest('fx-control', this);
25
26
  if (control) {
26
27
  control.refresh();
@@ -28,11 +29,13 @@ class FxRefresh extends AbstractAction {
28
29
  }
29
30
  }
30
31
  if(this.hasAttribute('force')){
32
+ console.log(`### <<<<< refresh() force ${this} >>>>>`);
31
33
  this.getOwnerForm().forceRefresh();
32
34
  return;
33
35
  }
34
36
  if(this.hasAttribute('control')){
35
37
  const targetId = this.getAttribute('control');
38
+ console.log(`### <<<<< refresh() control '${targetId}' >>>>>`);
36
39
  const ctrl = resolveId(targetId, this);
37
40
  if (ctrl && Fore.isUiElement(ctrl.nodeName) && typeof ctrl.refresh === 'function') {
38
41
  ctrl.refresh();
@@ -0,0 +1,68 @@
1
+ // import { FxAction } from './fx-action.js';
2
+ import '../fx-model.js';
3
+ import {AbstractAction} from './abstract-action.js';
4
+ import {evaluateXPathToString} from '../xpath-evaluation.js';
5
+ import {Fore} from '../fore.js';
6
+ import getInScopeContext from "../getInScopeContext";
7
+
8
+ /**
9
+ * `fx-setattribute` allows to create and set an attribute value in the data.
10
+ *
11
+ * @customElement
12
+ */
13
+ export default class FxSetattribute extends AbstractAction {
14
+ static get properties() {
15
+ return {
16
+ ...super.properties,
17
+ ref: {
18
+ type: String,
19
+ },
20
+ attrName: {
21
+ type: String,
22
+ },
23
+ attrValue:{
24
+ type: String
25
+ }
26
+ };
27
+ }
28
+
29
+ constructor() {
30
+ super();
31
+ this.ref = '';
32
+ this.attrName = '';
33
+ this.attrValue = '';
34
+ }
35
+
36
+ connectedCallback() {
37
+ if (super.connectedCallback) {
38
+ super.connectedCallback();
39
+ }
40
+
41
+ if (this.hasAttribute('ref')) {
42
+ this.ref = this.getAttribute('ref');
43
+ } else {
44
+ throw new Error('fx-setvalue must specify a "ref" attribute');
45
+ }
46
+ this.attrName = this.hasAttribute('name') ? this.getAttribute('name') : null;
47
+ this.attrValue = this.hasAttribute('value') ? this.getAttribute('value') : '';
48
+ if(!this.attrName){
49
+ Fore.dispatch('this', 'error',{message:'name or value not specified'});
50
+ }
51
+ }
52
+
53
+ async perform() {
54
+ super.perform();
55
+ const mi = this.getModelItem();
56
+ if(mi.node.nodeType !== Node.ELEMENT_NODE){
57
+ Fore.dispatch('this', 'error',{message:'referenced item is not an element'});
58
+ return;
59
+ }
60
+ mi.node.setAttribute(this.attrName, this.attrValue);
61
+ this.needsUpdate = true;
62
+ }
63
+
64
+ }
65
+
66
+ if (!customElements.get('fx-setattribute')) {
67
+ window.customElements.define('fx-setattribute', FxSetattribute);
68
+ }
@@ -32,7 +32,7 @@ class FxToggle extends AbstractAction {
32
32
  const fxSwitch = caseElement.parentNode;
33
33
  fxSwitch.toggle(caseElement);
34
34
  }
35
- // this.needsUpdate = true;
35
+ this.needsUpdate = true;
36
36
  }
37
37
  }
38
38