@jinntec/fore 3.2.1 → 3.3.1

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
@@ -1,3 +1,4 @@
1
+ import './tools/debug/auto-debugger.js';
1
2
  // core + models classes
2
3
  import './src/fx-bind.js';
3
4
  import './src/fx-connection.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "3.2.1",
3
+ "version": "3.3.1",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
@@ -93,13 +93,15 @@
93
93
  "test:prune-snapshots": "karma start --prune-snapshots",
94
94
  "test:bs": "karma start karma.bs.config.js --coverage",
95
95
  "start:build": "cd dist && es-dev-server --open",
96
- "build": "rimraf dist && rollup -c rollup.config.js",
96
+ "build": "rimraf dist && vite build --config vite.build.config.js && vite build --config vite.build-dev.config.js",
97
97
  "start": "vite --open --port 8090 --host",
98
98
  "preversion": "npm run test",
99
99
  "prepare": "npm run build",
100
100
  "install-demos": "npm i && cd demo && npm i",
101
101
  "start-cypress": "npm run start",
102
- "websocket-server": "node websocket-server.js"
102
+ "websocket-server": "node websocket-server.js",
103
+ "release": "bash scripts/release.sh",
104
+ "release:demo": "bash scripts/update-demo.sh"
103
105
  },
104
106
  "keywords": [
105
107
  "Fore",
@@ -66,6 +66,25 @@
66
66
  display: block;
67
67
  }
68
68
 
69
+ /*
70
+ * Native-constraint alert visibility: show fx-alert only after the user has
71
+ * interacted with the field (:user-invalid fires after first dirty interaction).
72
+ * Complements .visited[invalid] above, which covers Fore-driven (XPath) invalidity.
73
+ */
74
+ fx-control:has(input.widget:user-invalid) fx-alert {
75
+ display: block;
76
+ }
77
+
78
+ /*
79
+ * Fallback for browsers without :user-invalid support.
80
+ * :placeholder-shown is false when a value is present, approximating "user typed".
81
+ */
82
+ @supports not selector(input:user-invalid) {
83
+ fx-control[invalid]:has(input.widget:not(:placeholder-shown)) fx-alert {
84
+ display: block;
85
+ }
86
+ }
87
+
69
88
  /* case not displayed by default - if you want e.g. apply transitions you have to overwrite this rule with display='inline' or similar */
70
89
  fx-case {
71
90
  display: none;
@@ -73,11 +92,58 @@
73
92
  fx-case.selected-case{
74
93
  display:block;
75
94
  }
95
+ fx-case.deselected-case{
96
+ display:none;
97
+ }
76
98
 
77
99
  fx-output[readonly] img {
78
100
  background: inherit;
79
101
  }
80
102
 
103
+ /*
104
+ * Default visual treatment for readonly controls (own or inherited via
105
+ * <fx-bind readonly="...">). Individual apps can override.
106
+ */
107
+ fx-control[readonly] .widget,
108
+ fx-control[readonly] input,
109
+ fx-control[readonly] textarea,
110
+ fx-control[readonly] select {
111
+ background: var(--paper-grey-200);
112
+ color: var(--paper-grey-600);
113
+ cursor: not-allowed;
114
+ }
115
+
116
+ /*
117
+ * The native `readonly` attribute is a no-op on checkboxes/radios per the HTML
118
+ * spec - only `disabled` blocks them, but that would also mute their styling
119
+ * and remove them from focus order. Fore already rejects the resulting
120
+ * value-changed on the model side (see fx-control#setValue), but without this
121
+ * the box still visibly toggles and snaps back, reading as "still clickable".
122
+ * Blocking pointer interaction avoids that flicker entirely.
123
+ */
124
+ fx-control[readonly] input[type="checkbox"],
125
+ fx-control[readonly] input[type="radio"] {
126
+ pointer-events: none;
127
+ }
128
+
129
+ /*
130
+ * Clicking a <label for="..."> forwards a native activation to its checkbox/
131
+ * radio that bypasses the input's own `pointer-events: none` entirely - the
132
+ * browser toggles the control directly, it's not a hit-test on the input. The
133
+ * label itself must be blocked too, or the box still visibly flips on click.
134
+ */
135
+ fx-control[readonly]:has(input[type="checkbox"]) label,
136
+ fx-control[readonly]:has(input[type="radio"]) label {
137
+ pointer-events: none;
138
+ cursor: not-allowed;
139
+ }
140
+
141
+ fx-trigger[readonly] .widget,
142
+ fx-trigger[readonly] button {
143
+ cursor: not-allowed;
144
+ opacity: 0.6;
145
+ }
146
+
81
147
  .error {
82
148
  background: var(--paper-red-500);
83
149
  display: flex;
@@ -68,6 +68,24 @@ export default class ForeElementMixin extends HTMLElement {
68
68
  this.ownerForm = null;
69
69
  }
70
70
 
71
+ getDebugInfo() {
72
+ return {
73
+ localName: this.localName,
74
+ id: this.id || null,
75
+ ref: this.getAttribute?.('ref') || null,
76
+
77
+ modelItemPath: this.modelItem?.path || null,
78
+ instanceId: this.modelItem?.instanceId || null,
79
+
80
+ value: this.modelItem?.value,
81
+
82
+ relevant: this.modelItem?.relevant,
83
+ readonly: this.modelItem?.readonly,
84
+ required: this.modelItem?.required,
85
+ constraint: this.modelItem?.constraint,
86
+ };
87
+ }
88
+
71
89
  connectedCallback() {
72
90
  if (this.parentElement) {
73
91
  this.dependencies.setParentDependencies(this.parentElement?.closest('[ref]')?.dependencies);
@@ -186,10 +186,69 @@ export class AbstractAction extends ForeElementMixin {
186
186
  }
187
187
  }
188
188
 
189
+ getActionDebugDetail(extra = {}) {
190
+ const getAttr = name => {
191
+ try {
192
+ return this.getAttribute(name) || null;
193
+ } catch (error) {
194
+ return null;
195
+ }
196
+ };
197
+
198
+ return {
199
+ action: this.localName || null,
200
+ actionClass: this.constructor?.name || null,
201
+ id: this.id || null,
202
+ event: this.event || null,
203
+ ref: getAttr('ref'),
204
+ target: getAttr('target'),
205
+ origin: getAttr('origin'),
206
+ submission: getAttr('submission'),
207
+ control: getAttr('control'),
208
+ if: getAttr('if'),
209
+ while: getAttr('while'),
210
+ iterate: getAttr('iterate'),
211
+ delay: this.delay || 0,
212
+ needsUpdate: this.needsUpdate,
213
+ ownerFore: this.getOwnerFormSafe()?.id || null,
214
+ ...extra,
215
+ };
216
+ }
217
+
218
+ dispatchActionDebugEvent(type, extra = {}) {
219
+ try {
220
+ const target = this.isConnected ? this : (this.getOwnerFormSafe() ?? this);
221
+ target.dispatchEvent(
222
+ new CustomEvent(type, {
223
+ composed: true,
224
+ bubbles: true,
225
+ cancelable: false,
226
+ detail: this.getActionDebugDetail(extra),
227
+ }),
228
+ );
229
+ } catch (error) {
230
+ // Debug events must never affect action execution.
231
+ }
232
+ }
233
+
234
+ getOwnerFormSafe() {
235
+ try {
236
+ return this.getOwnerForm?.() || null;
237
+ } catch (error) {
238
+ return null;
239
+ }
240
+ }
241
+
189
242
  async performSafe() {
243
+ let success = false;
244
+
245
+ this.dispatchActionDebugEvent('action-start', {
246
+ phase: 'start',
247
+ });
248
+
190
249
  try {
191
250
  await this.perform();
192
- // Return true to indicate success
251
+ success = true;
193
252
  return true;
194
253
  } catch (error) {
195
254
  await Fore.dispatch(this, 'error', {
@@ -198,11 +257,15 @@ export class AbstractAction extends ForeElementMixin {
198
257
  expr: error,
199
258
  level: 'Error',
200
259
  });
201
- // Return false to indicate failure. Any loops must be canceled
260
+
202
261
  return false;
262
+ } finally {
263
+ this.dispatchActionDebugEvent('action-end', {
264
+ phase: 'end',
265
+ success,
266
+ });
203
267
  }
204
268
  }
205
-
206
269
  /**
207
270
  * executes the action.
208
271
  *
@@ -263,8 +326,10 @@ export class AbstractAction extends ForeElementMixin {
263
326
 
264
327
  // Outermost handling
265
328
  if (FxFore.outermostHandler === null) {
329
+ const ownerForm = this.getOwnerFormSafe();
330
+
266
331
  console.log(
267
- `%coutermost Action on ${this.getOwnerForm().id}`,
332
+ `%coutermost Action on ${ownerForm?.id || ''}`,
268
333
  'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
269
334
  this,
270
335
  );
@@ -319,7 +384,8 @@ export class AbstractAction extends ForeElementMixin {
319
384
  if (this.delay) {
320
385
  // Delay further execution until the delay is done
321
386
  await wait(this.delay);
322
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
387
+ const ownerForm = this.getOwnerFormSafe();
388
+ if (!ownerForm || !XPathUtil.contains(ownerForm, this)) {
323
389
  // We are no longer in the document. Stop working
324
390
  this.actionPerformed();
325
391
  resolveThisEvent();
@@ -336,7 +402,8 @@ export class AbstractAction extends ForeElementMixin {
336
402
  // Start by waiting
337
403
  await wait(this.delay || 0);
338
404
 
339
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
405
+ const ownerForm = this.getOwnerFormSafe();
406
+ if (!ownerForm || !XPathUtil.contains(ownerForm, this)) {
340
407
  // We are no longer in the document. Stop working
341
408
  return;
342
409
  }
@@ -347,7 +414,7 @@ export class AbstractAction extends ForeElementMixin {
347
414
  }
348
415
 
349
416
  // Perform the action once. But quit if it failed
350
- if (!this.performSafe()) {
417
+ if (!(await this.performSafe())) {
351
418
  return;
352
419
  }
353
420
 
@@ -373,7 +440,8 @@ export class AbstractAction extends ForeElementMixin {
373
440
  return;
374
441
  }
375
442
 
376
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
443
+ const ownerForm = this.getOwnerFormSafe();
444
+ if (!ownerForm || !XPathUtil.contains(ownerForm, this)) {
377
445
  // We are no longer in the document. Stop working
378
446
  return;
379
447
  }
@@ -400,8 +468,10 @@ export class AbstractAction extends ForeElementMixin {
400
468
  this.currentEvent = null;
401
469
  this.actionPerformed();
402
470
  if (FxFore.outermostHandler === this) {
471
+ const ownerForm = this.getOwnerFormSafe();
472
+
403
473
  console.log(
404
- `%cfinalizing outermost Action on ${this.getOwnerForm()?.id}`,
474
+ `%cfinalizing outermost Action on ${ownerForm?.id || ''}`,
405
475
  'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
406
476
  this,
407
477
  );
@@ -430,24 +500,22 @@ export class AbstractAction extends ForeElementMixin {
430
500
  * Template method to be implemented by each action that is called by execute() as part of
431
501
  * the processing.
432
502
  *
433
- * This function should not called on any action directly - call execute() instead to ensure proper execution of 'if' and 'while'
503
+ * This function should not called on any action directly - call execute() instead to ensure proper execution of 'if' and 'while'.
504
+ *
505
+ * TODO Fore DevTools:
506
+ * Concrete actions overriding perform() should call `await super.perform()` at the start.
507
+ * Otherwise the debugger will not see the `execute-action` event for that action.
434
508
  */
435
509
  async perform() {
436
- // await Fore.dispatch(document, 'execute-action', {action:this, event:this.event});
437
-
438
- // todo: review - this evaluation seems redundant as we already evaluated in execute
439
510
  if (this.isBound() || this.nodeName === 'FX-ACTION') {
440
511
  this.evalInContext();
441
512
  }
442
513
 
443
- this.dispatchEvent(
444
- new CustomEvent('execute-action', {
445
- composed: true,
446
- bubbles: true,
447
- cancelable: true,
448
- detail: { action: this, event: this.event },
449
- }),
450
- );
514
+ /*
515
+ await Fore.dispatch(this, 'execute-action', this.getActionDebugDetail({
516
+ phase: 'before',
517
+ }));
518
+ */
451
519
  }
452
520
 
453
521
  /**
@@ -475,7 +543,8 @@ export class AbstractAction extends ForeElementMixin {
475
543
  // console.log('running update cycle for outermostHandler', this);
476
544
  model.recalculate();
477
545
  model.revalidate();
478
- this.getOwnerForm().refresh(false);
546
+ const ownerForm = this.getOwnerFormSafe();
547
+ ownerForm?.refresh(false);
479
548
  this.dispatchActionPerformed();
480
549
  } else if (this.needsUpdate) {
481
550
  // console.log('Update delayed!');
@@ -493,8 +562,9 @@ export class AbstractAction extends ForeElementMixin {
493
562
  * @event action-performed - whenever an action has been run
494
563
  */
495
564
  dispatchActionPerformed() {
496
- // console.log('action-performed ', this);
497
- Fore.dispatch(this, 'action-performed', {});
565
+ Fore.dispatch(this, 'action-performed', this.getActionDebugDetail({
566
+ phase: 'after',
567
+ }));
498
568
  }
499
569
  }
500
570
 
@@ -413,7 +413,7 @@ export class FxInsert extends AbstractAction {
413
413
  xpath,
414
414
  });
415
415
 
416
- document.dispatchEvent(
416
+ this.dispatchEvent(
417
417
  new CustomEvent('index-changed', {
418
418
  composed: true,
419
419
  bubbles: true,
@@ -674,7 +674,7 @@ export class FxInsert extends AbstractAction {
674
674
  xpath,
675
675
  });
676
676
 
677
- document.dispatchEvent(
677
+ this.dispatchEvent(
678
678
  new CustomEvent('index-changed', {
679
679
  composed: true,
680
680
  bubbles: true,
@@ -7,6 +7,23 @@ const LOCAL_FUNCTIONS_NS = 'http://www.w3.org/2005/xquery-local-functions';
7
7
  // Keyed by resolved URL + prefix + mode.
8
8
  const _functionLibLoadCache = new Map();
9
9
 
10
+ // registerFunction() enforces "first wins" for conflicting names, but library sources
11
+ // (module imports, fetches) resolve asynchronously in whatever order the network/parser
12
+ // finishes them in. To make "first" mean "first in document order" (deterministic, and
13
+ // independent of load timing) rather than "first promise to resolve" (racy), every real
14
+ // loader reserves its turn synchronously in connectedCallback -- before any await -- and
15
+ // only applies its registrations once all earlier-reserved loaders have applied theirs.
16
+ let _registrationOrderChain = Promise.resolve();
17
+
18
+ function _reserveRegistrationTurn() {
19
+ const waitForTurn = _registrationOrderChain;
20
+ let releaseTurn;
21
+ _registrationOrderChain = new Promise(resolve => {
22
+ releaseTurn = resolve;
23
+ });
24
+ return { waitForTurn, releaseTurn };
25
+ }
26
+
10
27
  function looksLikeModuleSrc(src) {
11
28
  return /\.m?js($|\?)/i.test(src);
12
29
  }
@@ -87,11 +104,19 @@ export class FxFunctionlib extends ForeElementMixin {
87
104
  return;
88
105
  }
89
106
 
107
+ // Reserve our place in the registration order now, synchronously, so it reflects
108
+ // document order rather than whichever loader's fetch/import happens to finish first.
109
+ const { waitForTurn, releaseTurn } = _reserveRegistrationTurn();
110
+
90
111
  const loadPromise = (async () => {
91
- if (isModule) {
92
- await this._loadModuleLibrary(resolvedUrl, src, prefix);
93
- } else {
94
- await this._loadHtmlLibrary(resolvedUrl, src, prefix);
112
+ try {
113
+ if (isModule) {
114
+ await this._loadModuleLibrary(resolvedUrl, src, prefix, waitForTurn);
115
+ } else {
116
+ await this._loadHtmlLibrary(resolvedUrl, src, prefix, waitForTurn);
117
+ }
118
+ } finally {
119
+ releaseTurn();
95
120
  }
96
121
  })();
97
122
 
@@ -130,11 +155,13 @@ export class FxFunctionlib extends ForeElementMixin {
130
155
  registerFunction({ ...functionObject, signature: sig }, this);
131
156
  }
132
157
 
133
- async _loadModuleLibrary(resolvedUrl, src, prefix) {
158
+ async _loadModuleLibrary(resolvedUrl, src, prefix, waitForTurn) {
134
159
  const mod = await import(/* @vite-ignore */ resolvedUrl);
135
160
 
136
161
  const items = normalizeModuleExportToList(mod, src);
137
162
 
163
+ await waitForTurn;
164
+
138
165
  for (const item of items) {
139
166
  if (typeof item === 'function') {
140
167
  const { signature } = item;
@@ -154,7 +181,7 @@ export class FxFunctionlib extends ForeElementMixin {
154
181
  }
155
182
  }
156
183
 
157
- async _loadHtmlLibrary(resolvedUrl, src, prefix) {
184
+ async _loadHtmlLibrary(resolvedUrl, src, prefix, waitForTurn) {
158
185
  const result = await fetch(resolvedUrl);
159
186
  if (!result.ok) {
160
187
  console.error(`Loading function library at ${src} failed.`);
@@ -165,6 +192,9 @@ export class FxFunctionlib extends ForeElementMixin {
165
192
  const document = new DOMParser().parseFromString(body, 'text/html');
166
193
 
167
194
  const functions = Array.from(document.querySelectorAll('fx-function'));
195
+
196
+ await waitForTurn;
197
+
168
198
  for (const func of functions) {
169
199
  const functionObject = {
170
200
  type: func.getAttribute('type'),
package/src/fx-bind.js CHANGED
@@ -45,6 +45,34 @@ export class FxBind extends ForeElementMixin {
45
45
  this.inited = false;
46
46
  }
47
47
 
48
+ getDebugInfo() {
49
+ return {
50
+ ref: this.getAttribute('ref'),
51
+ id: this.id || null,
52
+ instanceId: this.instanceId,
53
+ bindType: this.bindType,
54
+
55
+ calculate: this.getAttribute('calculate'),
56
+ readonly: this.getAttribute('readonly'),
57
+ required: this.getAttribute('required'),
58
+ relevant: this.getAttribute('relevant'),
59
+ constraint: this.getAttribute('constraint'),
60
+ type: this.getAttribute('type'),
61
+
62
+ modelItemCount: this._debugModelItemCount?.() || null,
63
+ };
64
+ }
65
+
66
+ _debugModelItemCount() {
67
+ const model = this.getModel?.();
68
+
69
+ if (!model || !Array.isArray(model.modelItems)) {
70
+ return 0;
71
+ }
72
+
73
+ return model.modelItems.filter(item => item?.bind === this).length;
74
+ }
75
+
48
76
  connectedCallback() {
49
77
  // console.log('connectedCallback ', this);
50
78
  // this.id = this.hasAttribute('id')?this.getAttribute('id'):;