@jinntec/fore 3.0.1 → 3.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/README.md +0 -2
- package/dist/fore-dev.js +5019 -4180
- package/dist/fore.js +4767 -4165
- package/index.js +1 -0
- package/package.json +2 -2
- package/src/actions/fx-insert.js +3 -2
- package/src/authoring-check.js +231 -0
- package/src/fore.js +9 -11
- package/src/fx-fore.js +488 -147
- package/src/fx-instance.js +7 -1
- package/src/fx-speech.js +268 -0
- package/src/ui/abstract-control.js +14 -7
- package/src/ui/fx-case.js +3 -1
- package/src/ui/fx-control.js +4 -2
- package/src/ui/fx-group.js +1 -1
- package/src/ui/fx-output.js +8 -38
- package/src/ui/fx-repeat-attributes.js +1 -1
- package/src/ui/fx-repeat.js +1 -1
- package/src/ui/repeat-base.js +4 -4
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinntec/fore",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Fore - declarative user interfaces in plain HTML",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@jinntec/jinn-toast": "^1.0.5",
|
|
34
34
|
"@picocss/pico": "^2.0.6",
|
|
35
35
|
"chai": "^5.2.1",
|
|
36
|
-
"fontoxpath": "^3.
|
|
36
|
+
"fontoxpath": "^3.34.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@babel/plugin-proposal-class-properties": "^7.17.12",
|
package/src/actions/fx-insert.js
CHANGED
|
@@ -111,8 +111,9 @@ export class FxInsert extends AbstractAction {
|
|
|
111
111
|
_getJsonTemplateTextFromTemplateEl(tplEl) {
|
|
112
112
|
if (!tplEl) return null;
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
|
|
114
|
+
// <template>.textContent is "" because content lives in template.content (DocumentFragment).
|
|
115
|
+
// innerHTML reads from the content fragment correctly.
|
|
116
|
+
const raw = String(tplEl.innerHTML || tplEl.textContent || '').trim();
|
|
116
117
|
if (!raw) return null;
|
|
117
118
|
return raw;
|
|
118
119
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring integrity checks for Fore forms.
|
|
3
|
+
*
|
|
4
|
+
* Runs by default at startup. Add the `no-check` attribute to `<fx-fore>` to disable
|
|
5
|
+
* (e.g. in production). The module is dynamically imported, so it is never loaded
|
|
6
|
+
* when checks are disabled.
|
|
7
|
+
*
|
|
8
|
+
* Adding a new check: add a function `_check<Name>(fore, errors)` and call it in
|
|
9
|
+
* `checkAuthoring()` below.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const INSTANCE_RE = /instance\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
13
|
+
const INDEX_RE = /index\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
14
|
+
|
|
15
|
+
// Attributes that may carry XPath expressions
|
|
16
|
+
const XPATH_ATTRS = [
|
|
17
|
+
'ref',
|
|
18
|
+
'value',
|
|
19
|
+
'calculate',
|
|
20
|
+
'constraint',
|
|
21
|
+
'required',
|
|
22
|
+
'readonly',
|
|
23
|
+
'relevant',
|
|
24
|
+
'bind',
|
|
25
|
+
'context',
|
|
26
|
+
'if',
|
|
27
|
+
'while',
|
|
28
|
+
'origin',
|
|
29
|
+
'iterate',
|
|
30
|
+
'at',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function _isDynamic(val) {
|
|
34
|
+
return !val || val.includes('{');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _byId(fore, id) {
|
|
38
|
+
return (
|
|
39
|
+
fore.ownerDocument.getElementById(id) ||
|
|
40
|
+
fore.getRootNode().getElementById?.(id) ||
|
|
41
|
+
fore.querySelector(`#${id}`)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _checkSendSubmissions(fore, errors) {
|
|
46
|
+
fore.querySelectorAll('fx-send[submission]').forEach(el => {
|
|
47
|
+
const id = el.getAttribute('submission');
|
|
48
|
+
if (_isDynamic(id)) return;
|
|
49
|
+
const localFore = el.closest('fx-fore');
|
|
50
|
+
const { model } = localFore;
|
|
51
|
+
const target = model
|
|
52
|
+
? model.querySelector(`fx-submission#${id}`)
|
|
53
|
+
: fore.querySelector(`fx-submission#${id}`);
|
|
54
|
+
if (!target) {
|
|
55
|
+
errors.push({
|
|
56
|
+
element: el,
|
|
57
|
+
message: `<fx-send submission="${id}">: no <fx-submission id="${id}"> found`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _checkDispatchTargets(fore, errors) {
|
|
64
|
+
fore.querySelectorAll('fx-dispatch[targetid]').forEach(el => {
|
|
65
|
+
const id = el.getAttribute('targetid');
|
|
66
|
+
if (_isDynamic(id)) return;
|
|
67
|
+
if (!_byId(fore, id)) {
|
|
68
|
+
errors.push({
|
|
69
|
+
element: el,
|
|
70
|
+
message: `<fx-dispatch targetid="${id}">: no element with id="${id}" found`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _checkXPathInstanceRefs(fore, errors) {
|
|
77
|
+
const allEls = Array.from(fore.querySelectorAll('*'));
|
|
78
|
+
for (const el of allEls) {
|
|
79
|
+
const localFore = el.closest('fx-fore');
|
|
80
|
+
for (const attr of XPATH_ATTRS) {
|
|
81
|
+
const val = el.getAttribute(attr);
|
|
82
|
+
if (!val) continue;
|
|
83
|
+
|
|
84
|
+
INSTANCE_RE.lastIndex = 0;
|
|
85
|
+
let m;
|
|
86
|
+
while ((m = INSTANCE_RE.exec(val)) !== null) {
|
|
87
|
+
const id = m[1];
|
|
88
|
+
const localInstance = localFore.querySelector(`fx-instance#${id}`);
|
|
89
|
+
const sharedInstance =
|
|
90
|
+
!localInstance && localFore.ownerDocument.querySelector(`fx-instance[shared]#${id}`);
|
|
91
|
+
if (!localInstance && !sharedInstance) {
|
|
92
|
+
errors.push({
|
|
93
|
+
element: el,
|
|
94
|
+
message: `[${attr}="${val}"]: instance('${id}') — no <fx-instance id="${id}"> found`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
INDEX_RE.lastIndex = 0;
|
|
100
|
+
while ((m = INDEX_RE.exec(val)) !== null) {
|
|
101
|
+
const id = m[1];
|
|
102
|
+
if (!localFore.querySelector(`fx-repeat#${id}`)) {
|
|
103
|
+
errors.push({
|
|
104
|
+
element: el,
|
|
105
|
+
message: `[${attr}="${val}"]: index('${id}') — no <fx-repeat id="${id}"> found`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _checkCallActions(fore, errors) {
|
|
114
|
+
fore.querySelectorAll('fx-call[action]').forEach(el => {
|
|
115
|
+
const id = el.getAttribute('action');
|
|
116
|
+
if (_isDynamic(id)) return;
|
|
117
|
+
if (!_byId(fore, id)) {
|
|
118
|
+
errors.push({
|
|
119
|
+
element: el,
|
|
120
|
+
message: `<fx-call action="${id}">: no element with id="${id}" found`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function _checkShowHideDialogs(fore, errors) {
|
|
127
|
+
fore.querySelectorAll('fx-show[dialog], fx-hide[dialog]').forEach(el => {
|
|
128
|
+
const id = el.getAttribute('dialog');
|
|
129
|
+
if (_isDynamic(id)) return;
|
|
130
|
+
if (!_byId(fore, id)) {
|
|
131
|
+
errors.push({
|
|
132
|
+
element: el,
|
|
133
|
+
message: `<${el.localName} dialog="${id}">: no element with id="${id}" found`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _checkLoadAttachTo(fore, errors) {
|
|
140
|
+
fore.querySelectorAll('fx-load[attach-to]').forEach(el => {
|
|
141
|
+
const val = el.getAttribute('attach-to');
|
|
142
|
+
if (_isDynamic(val)) return;
|
|
143
|
+
if (!val.startsWith('#')) return; // _blank, _self etc. are valid non-id targets
|
|
144
|
+
const id = val.substring(1);
|
|
145
|
+
if (!_byId(fore, id)) {
|
|
146
|
+
errors.push({
|
|
147
|
+
element: el,
|
|
148
|
+
message: `<fx-load attach-to="${val}">: no element with id="${id}" found`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _checkRefreshControl(fore, errors) {
|
|
155
|
+
fore.querySelectorAll('fx-refresh[control]').forEach(el => {
|
|
156
|
+
const id = el.getAttribute('control');
|
|
157
|
+
if (_isDynamic(id)) return;
|
|
158
|
+
if (!_byId(fore, id)) {
|
|
159
|
+
errors.push({
|
|
160
|
+
element: el,
|
|
161
|
+
message: `<fx-refresh control="${id}">: no element with id="${id}" found`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _checkResetInstance(fore, errors) {
|
|
168
|
+
const model = fore.querySelector(':scope > fx-model');
|
|
169
|
+
fore.querySelectorAll('fx-reset[instance]').forEach(el => {
|
|
170
|
+
const id = el.getAttribute('instance');
|
|
171
|
+
if (_isDynamic(id)) return;
|
|
172
|
+
const target = model
|
|
173
|
+
? model.querySelector(`fx-instance#${id}`)
|
|
174
|
+
: fore.querySelector(`fx-instance#${id}`);
|
|
175
|
+
const sharedTarget = !target && fore.ownerDocument.querySelector(`fx-instance[shared]#${id}`);
|
|
176
|
+
if (!target && !sharedTarget) {
|
|
177
|
+
errors.push({
|
|
178
|
+
element: el,
|
|
179
|
+
message: `<fx-reset instance="${id}">: no <fx-instance id="${id}"> found`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _checkSetfocusControl(fore, errors) {
|
|
186
|
+
fore.querySelectorAll('fx-setfocus[control]').forEach(el => {
|
|
187
|
+
const id = el.getAttribute('control');
|
|
188
|
+
if (_isDynamic(id)) return;
|
|
189
|
+
if (!_byId(fore, id)) {
|
|
190
|
+
errors.push({
|
|
191
|
+
element: el,
|
|
192
|
+
message: `<fx-setfocus control="${id}">: no element with id="${id}" found`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _checkToggleCase(fore, errors) {
|
|
199
|
+
fore.querySelectorAll('fx-toggle[case]').forEach(el => {
|
|
200
|
+
const id = el.getAttribute('case');
|
|
201
|
+
if (_isDynamic(id)) return;
|
|
202
|
+
if (!fore.querySelector(`fx-case#${id}`)) {
|
|
203
|
+
errors.push({
|
|
204
|
+
element: el,
|
|
205
|
+
message: `<fx-toggle case="${id}">: no <fx-case id="${id}"> found`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run all authoring checks on a given `<fx-fore>` element.
|
|
213
|
+
* Returns an array of `{ element, message }` error objects.
|
|
214
|
+
*
|
|
215
|
+
* @param {HTMLElement} fore
|
|
216
|
+
* @returns {{ element: HTMLElement, message: string }[]}
|
|
217
|
+
*/
|
|
218
|
+
export function checkAuthoring(fore) {
|
|
219
|
+
const errors = [];
|
|
220
|
+
_checkSendSubmissions(fore, errors);
|
|
221
|
+
_checkDispatchTargets(fore, errors);
|
|
222
|
+
_checkXPathInstanceRefs(fore, errors);
|
|
223
|
+
_checkCallActions(fore, errors);
|
|
224
|
+
_checkShowHideDialogs(fore, errors);
|
|
225
|
+
_checkLoadAttachTo(fore, errors);
|
|
226
|
+
_checkRefreshControl(fore, errors);
|
|
227
|
+
_checkResetInstance(fore, errors);
|
|
228
|
+
_checkSetfocusControl(fore, errors);
|
|
229
|
+
_checkToggleCase(fore, errors);
|
|
230
|
+
return errors;
|
|
231
|
+
}
|
package/src/fore.js
CHANGED
|
@@ -17,7 +17,7 @@ export class Fore {
|
|
|
17
17
|
|
|
18
18
|
static REQUIRED_DEFAULT = false;
|
|
19
19
|
|
|
20
|
-
static RELEVANT_DEFAULT = true
|
|
20
|
+
static RELEVANT_DEFAULT = true;
|
|
21
21
|
|
|
22
22
|
static CONSTRAINT_DEFAULT = true;
|
|
23
23
|
|
|
@@ -267,7 +267,6 @@ export class Fore {
|
|
|
267
267
|
return ['FX-BIND', 'FX-FUNCTION', 'FX-MODEL', 'FX-INSTANCE', 'FX-SUBMISSION'];
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
|
|
271
270
|
static async initUI(startElement) {
|
|
272
271
|
const inited = new Promise(resolve => {
|
|
273
272
|
const { children } = startElement;
|
|
@@ -301,23 +300,25 @@ export class Fore {
|
|
|
301
300
|
}
|
|
302
301
|
|
|
303
302
|
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
|
|
304
|
-
/** @type {import('./
|
|
303
|
+
/** @type {import('./ui/UIElement.js').UIElement} */
|
|
305
304
|
const bound = element;
|
|
306
305
|
|
|
307
306
|
// Keep old behavior: only refresh UI elements during full/forced refresh
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
} else if (force === true) {
|
|
307
|
+
// Any #refresh call does its own recursion.
|
|
308
|
+
if (force === true) {
|
|
311
309
|
const maybePromise = bound.refresh(force);
|
|
312
310
|
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
313
311
|
await maybePromise;
|
|
314
312
|
}
|
|
315
|
-
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (typeof force === 'object') {
|
|
316
316
|
// future selective refresh logic can live here if you re-enable it
|
|
317
317
|
const maybePromise = bound.refresh(force);
|
|
318
318
|
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
319
319
|
await maybePromise;
|
|
320
320
|
}
|
|
321
|
+
continue;
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
|
|
@@ -521,10 +522,7 @@ export class Fore {
|
|
|
521
522
|
const reg = /(>)(<)(\/*)/g;
|
|
522
523
|
const wsexp = / *(.*) +\n/g;
|
|
523
524
|
const contexp = /(<.+>)(.+\n)/g;
|
|
524
|
-
xml = xml
|
|
525
|
-
.replace(reg, '$1\n$2$3')
|
|
526
|
-
.replace(wsexp, '$1\n')
|
|
527
|
-
.replace(contexp, '$1\n$2');
|
|
525
|
+
xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2');
|
|
528
526
|
let formatted = '';
|
|
529
527
|
const lines = xml.split('\n');
|
|
530
528
|
let indent = 0;
|