@jinntec/fore 3.3.0 → 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/dist/fore-dev.js +825 -782
- package/dist/fore.js +1937 -1894
- package/package.json +4 -3
- package/resources/fore.css +44 -0
- package/src/functions/fx-functionlib.js +36 -6
- package/src/fx-model.js +1 -0
- package/src/modelitem.js +77 -2
- package/src/ui/abstract-control.js +15 -4
- package/src/ui/fx-control.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinntec/fore",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Fore - declarative user interfaces in plain HTML",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -94,13 +94,14 @@
|
|
|
94
94
|
"test:bs": "karma start karma.bs.config.js --coverage",
|
|
95
95
|
"start:build": "cd dist && es-dev-server --open",
|
|
96
96
|
"build": "rimraf dist && vite build --config vite.build.config.js && vite build --config vite.build-dev.config.js",
|
|
97
|
-
"build:rollup": "rimraf dist && rollup -c rollup.config.js",
|
|
98
97
|
"start": "vite --open --port 8090 --host",
|
|
99
98
|
"preversion": "npm run test",
|
|
100
99
|
"prepare": "npm run build",
|
|
101
100
|
"install-demos": "npm i && cd demo && npm i",
|
|
102
101
|
"start-cypress": "npm run start",
|
|
103
|
-
"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"
|
|
104
105
|
},
|
|
105
106
|
"keywords": [
|
|
106
107
|
"Fore",
|
package/resources/fore.css
CHANGED
|
@@ -100,6 +100,50 @@
|
|
|
100
100
|
background: inherit;
|
|
101
101
|
}
|
|
102
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
|
+
|
|
103
147
|
.error {
|
|
104
148
|
background: var(--paper-red-500);
|
|
105
149
|
display: flex;
|
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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-model.js
CHANGED
|
@@ -406,6 +406,7 @@ export class FxModel extends HTMLElement {
|
|
|
406
406
|
// Tabula rasa for computed facets; keep identity (boundControls/observers)
|
|
407
407
|
mi.readonly = ModelItem.READONLY_DEFAULT;
|
|
408
408
|
mi.relevant = ModelItem.RELEVANT_DEFAULT;
|
|
409
|
+
mi._parentModelItem = undefined;
|
|
409
410
|
mi.required = ModelItem.REQUIRED_DEFAULT;
|
|
410
411
|
mi.constraint = ModelItem.CONSTRAINT_DEFAULT;
|
|
411
412
|
mi.type = ModelItem.TYPE_DEFAULT;
|
package/src/modelitem.js
CHANGED
|
@@ -28,8 +28,10 @@ export class ModelItem {
|
|
|
28
28
|
constructor(path, ref, nodeOrLens, bind, instance, fore) {
|
|
29
29
|
this.path = path;
|
|
30
30
|
this.ref = ref;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
31
|
+
this._readonly = ModelItem.READONLY_DEFAULT;
|
|
32
|
+
this._relevant = ModelItem.RELEVANT_DEFAULT;
|
|
33
|
+
// undefined = not yet resolved; null = resolved, no ancestor ModelItem found
|
|
34
|
+
this._parentModelItem = undefined;
|
|
33
35
|
this.required = ModelItem.REQUIRED_DEFAULT;
|
|
34
36
|
this.constraint = ModelItem.CONSTRAINT_DEFAULT;
|
|
35
37
|
this.type = ModelItem.TYPE_DEFAULT;
|
|
@@ -65,6 +67,79 @@ export class ModelItem {
|
|
|
65
67
|
this.state = {}; // evaluated expression results
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* `readonly` is inherited down the instance tree per XForms semantics: a node is
|
|
72
|
+
* readonly if its own bind says so, or if any ancestor node is readonly.
|
|
73
|
+
*/
|
|
74
|
+
get readonly() {
|
|
75
|
+
return this._readonly || !!this.getParentModelItem()?.readonly;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set readonly(val) {
|
|
79
|
+
this._readonly = val;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `relevant` is inherited down the instance tree per XForms semantics: a node is
|
|
84
|
+
* relevant only if its own bind says so AND its parent is (effectively) relevant.
|
|
85
|
+
*/
|
|
86
|
+
get relevant() {
|
|
87
|
+
if (!this._relevant) return false;
|
|
88
|
+
const parent = this.getParentModelItem();
|
|
89
|
+
return parent ? parent.relevant : true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
set relevant(val) {
|
|
93
|
+
this._relevant = val;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolves and caches the nearest ancestor ModelItem, walking past instance
|
|
98
|
+
* nodes that have no ModelItem of their own. Handles both XML DOM nodes
|
|
99
|
+
* (`node.parentNode`) and JSON lens nodes (`lens.parent`).
|
|
100
|
+
* @returns {ModelItem|null}
|
|
101
|
+
*/
|
|
102
|
+
getParentModelItem() {
|
|
103
|
+
if (this._parentModelItem !== undefined) return this._parentModelItem;
|
|
104
|
+
|
|
105
|
+
// `bind` is only set for ModelItems created from an explicit <fx-bind ref="...">.
|
|
106
|
+
// Lazily-created ModelItems (e.g. controls without a dedicated bind - the exact
|
|
107
|
+
// case this inheritance walk needs to cover) have no `bind`, so fall back to
|
|
108
|
+
// resolving the owning FxModel through the fx-fore element instead.
|
|
109
|
+
const model = this.bind?.model || this.fore?.getModel?.();
|
|
110
|
+
// `Attr.parentNode` is always null per the DOM spec - attribute nodes only
|
|
111
|
+
// expose their owning element via `ownerElement`. Without this, readonly/
|
|
112
|
+
// relevant inheritance silently breaks for every attribute-bound ModelItem.
|
|
113
|
+
let current;
|
|
114
|
+
if (this.lens) {
|
|
115
|
+
current = this.lens.parent;
|
|
116
|
+
} else if (this.node?.nodeType === Node.ATTRIBUTE_NODE) {
|
|
117
|
+
current = this.node.ownerElement;
|
|
118
|
+
} else {
|
|
119
|
+
current = this.node?.parentNode;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
while (current) {
|
|
123
|
+
if (
|
|
124
|
+
current.nodeType === Node.DOCUMENT_NODE ||
|
|
125
|
+
current.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
126
|
+
) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const mi = model?.getModelItem(current);
|
|
131
|
+
if (mi) {
|
|
132
|
+
this._parentModelItem = mi;
|
|
133
|
+
return mi;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
current = current.__jsonlens__ === true ? current.parent : current.parentNode;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._parentModelItem = null;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
68
143
|
getDebugInfo() {
|
|
69
144
|
return {
|
|
70
145
|
path: this.path,
|
|
@@ -320,13 +320,13 @@ export default class AbstractControl extends UIElement {
|
|
|
320
320
|
|
|
321
321
|
handleReadonly() {
|
|
322
322
|
// console.log('mip readonly', this.modelItem.isReadonly);
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
const effectiveReadonly = this.modelItem.readonly || this.isHostReadonly();
|
|
324
|
+
if (this.isReadonly() !== effectiveReadonly) {
|
|
325
|
+
if (effectiveReadonly) {
|
|
325
326
|
this.widget.setAttribute('readonly', '');
|
|
326
327
|
this.setAttribute('readonly', '');
|
|
327
328
|
this._dispatchEvent('readonly');
|
|
328
|
-
}
|
|
329
|
-
if (!this.modelItem.readonly) {
|
|
329
|
+
} else {
|
|
330
330
|
this.widget.removeAttribute('readonly');
|
|
331
331
|
this.removeAttribute('readonly');
|
|
332
332
|
this._dispatchEvent('readwrite');
|
|
@@ -334,6 +334,17 @@ export default class AbstractControl extends UIElement {
|
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
/**
|
|
338
|
+
* A control embedded via `src` (a whole nested `fx-fore` used as widget) has its
|
|
339
|
+
* own independent model, so it cannot see the readonly state of the outer
|
|
340
|
+
* ModelItem it represents. When the outer control becomes readonly, it marks its
|
|
341
|
+
* widget (the nested `fx-fore`) with a `readonly` attribute; nested controls pick
|
|
342
|
+
* that up here by checking their nearest enclosing `fx-fore`.
|
|
343
|
+
*/
|
|
344
|
+
isHostReadonly() {
|
|
345
|
+
return !!this.closest('fx-fore')?.hasAttribute('readonly');
|
|
346
|
+
}
|
|
347
|
+
|
|
337
348
|
// todo - review alert handling altogether. There could be potentially multiple ones in model
|
|
338
349
|
// TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code
|
|
339
350
|
handleValid() {
|
package/src/ui/fx-control.js
CHANGED
|
@@ -545,6 +545,11 @@ export default class FxControl extends XfAbstractControl {
|
|
|
545
545
|
dummy.replaceWith(imported);
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
// `this.widget` was cached as the placeholder `dummy` input on init(); that
|
|
549
|
+
// node is now detached, so readonly/required handling would silently target
|
|
550
|
+
// a removed element. Point it at the embedded fx-fore, which now acts as the widget.
|
|
551
|
+
this.widget = imported;
|
|
552
|
+
|
|
548
553
|
if (!theFore) {
|
|
549
554
|
Fore.dispatch('error', {
|
|
550
555
|
detail: {
|