@jinntec/fore 3.3.1 → 4.0.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 +98 -70
- package/dist/fore-dev.js +5593 -6832
- package/dist/fore.js +5602 -4887
- package/index.js +5 -10
- package/package.json +7 -7
- package/resources/fore.css +33 -0
- package/src/DependencyNotifyingDomFacade.js +90 -21
- package/src/DependentXPathQueries.js +15 -2
- package/src/ForeElementMixin.js +110 -16
- package/src/UndoManager.js +267 -0
- package/src/actions/abstract-action.js +71 -30
- package/src/actions/fx-action.js +5 -0
- package/src/actions/fx-append.js +3 -3
- package/src/actions/fx-commit-history.js +26 -0
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +25 -22
- package/src/actions/fx-load.js +5 -5
- package/src/actions/fx-redo.js +58 -0
- package/src/actions/fx-refresh.js +2 -2
- package/src/actions/fx-reload.js +1 -1
- package/src/actions/fx-replace.js +1 -1
- package/src/actions/fx-send.js +27 -5
- package/src/actions/fx-setattribute.js +11 -7
- package/src/actions/fx-undo.js +58 -0
- package/src/createNodes.js +314 -0
- package/src/fore.js +53 -18
- package/src/functions/fx-functionlib.js +10 -10
- package/src/fx-bind.js +30 -18
- package/src/fx-fore.js +222 -200
- package/src/fx-instance.js +18 -1
- package/src/fx-model.js +236 -69
- package/src/fx-submission.js +37 -29
- package/src/fx-var.js +49 -13
- package/src/getInScopeContext.js +1 -1
- package/src/json/JSONDomFacade.js +1 -1
- package/src/json/JSONLens.js +2 -2
- package/src/ui/UIElement.js +18 -8
- package/src/ui/abstract-control.js +45 -3
- package/src/ui/fx-alert.js +4 -0
- package/src/ui/fx-case.js +1 -1
- package/src/ui/fx-container.js +3 -0
- package/src/ui/fx-control-menu.js +79 -11
- package/src/ui/fx-control.js +130 -41
- package/src/ui/fx-dialog.js +5 -0
- package/src/ui/fx-items.js +6 -6
- package/src/ui/fx-output.js +37 -1
- package/src/ui/fx-repeat.js +1065 -103
- package/src/ui/fx-repeatitem.js +4 -1
- package/src/ui/fx-switch.js +116 -3
- package/src/ui/fx-trigger.js +9 -4
- package/src/ui/fx-upload.js +10 -4
- package/src/ui/repeat-base.js +20 -12
- package/src/withDraggability.js +10 -1
- package/src/xpath-evaluation.js +30 -18
- package/src/xpath-path.js +122 -0
- package/src/xpath-util.js +11 -126
- package/src/actions/StringTpl.js +0 -17
- package/src/extract-predicate-deps.js +0 -57
- package/src/extractPredicateDependencies.js +0 -36
- package/src/json/lensFromNode.js +0 -5
- package/src/json-util.js +0 -27
- package/src/tools/adi.js +0 -1111
- package/src/tools/deprecation.md +0 -1
- package/src/tools/fx-action-log.js +0 -745
- package/src/tools/fx-devtools.js +0 -444
- package/src/tools/fx-dom-inspector.js +0 -610
- package/src/tools/fx-json-instance.js +0 -444
- package/src/tools/fx-log-item.js +0 -128
- package/src/tools/fx-log-settings.js +0 -533
- package/src/tools/fx-minimap.js +0 -203
- package/src/tools/helpers.js +0 -132
- package/src/ui/TemplateExpression.js +0 -12
- package/src/ui/fx-dom-inspector.js +0 -1255
package/src/fx-submission.js
CHANGED
|
@@ -152,7 +152,9 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
152
152
|
|
|
153
153
|
const instance = this.getInstance();
|
|
154
154
|
if (!instance) {
|
|
155
|
-
Fore.dispatch(this, 'warn', {
|
|
155
|
+
Fore.dispatch(this, 'warn', {
|
|
156
|
+
message: `instance not found ${instance?.getAttribute?.('id')}`,
|
|
157
|
+
});
|
|
156
158
|
}
|
|
157
159
|
const instType = instance.getAttribute('type');
|
|
158
160
|
|
|
@@ -244,8 +246,8 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
244
246
|
|
|
245
247
|
if (!response.ok || response.status > 400) {
|
|
246
248
|
console.info(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
`%csubmit-error #${this.id}`,
|
|
250
|
+
'background:red; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
249
251
|
);
|
|
250
252
|
|
|
251
253
|
Fore.dispatch(this, 'submit-error', {
|
|
@@ -322,13 +324,14 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
322
324
|
// If no payload was passed, derive it from instance default context/nodeset
|
|
323
325
|
if (payload == null && instanceEl) {
|
|
324
326
|
payload =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
327
|
+
(typeof instanceEl.getDefaultContext === 'function' && instanceEl.getDefaultContext()) ||
|
|
328
|
+
instanceEl.nodeset ||
|
|
329
|
+
null;
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
// Decide JSON vs XML by instance type (NOT by ref expression)
|
|
331
|
-
const isJsonInstance =
|
|
333
|
+
const isJsonInstance =
|
|
334
|
+
instanceEl?.getAttribute?.('type') === 'json' || instanceEl?.type === 'json';
|
|
332
335
|
|
|
333
336
|
if (isJsonInstance) {
|
|
334
337
|
// Convert JSON lens nodes to plain JS before stringify
|
|
@@ -346,8 +349,7 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
346
349
|
try {
|
|
347
350
|
if (payload && payload.nodeType) {
|
|
348
351
|
// Document => serialize documentElement, Node => serialize node
|
|
349
|
-
const node =
|
|
350
|
-
payload.nodeType === Node.DOCUMENT_NODE ? payload.documentElement : payload;
|
|
352
|
+
const node = payload.nodeType === Node.DOCUMENT_NODE ? payload.documentElement : payload;
|
|
351
353
|
return new XMLSerializer().serializeToString(node);
|
|
352
354
|
}
|
|
353
355
|
} catch (_e) {
|
|
@@ -410,11 +412,11 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
410
412
|
const val = node.value;
|
|
411
413
|
|
|
412
414
|
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
415
|
+
val === null ||
|
|
416
|
+
val === undefined ||
|
|
417
|
+
typeof val === 'string' ||
|
|
418
|
+
typeof val === 'number' ||
|
|
419
|
+
typeof val === 'boolean'
|
|
418
420
|
) {
|
|
419
421
|
return val ?? null;
|
|
420
422
|
}
|
|
@@ -445,7 +447,6 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
445
447
|
|
|
446
448
|
return result;
|
|
447
449
|
}
|
|
448
|
-
1
|
|
449
450
|
// Fallback: last-resort scalar conversion
|
|
450
451
|
try {
|
|
451
452
|
if (typeof node.get === 'function') {
|
|
@@ -529,14 +530,14 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
529
530
|
|
|
530
531
|
if (this.targetref) {
|
|
531
532
|
const [theTarget] = evaluateXPath(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
533
|
+
this.targetref,
|
|
534
|
+
targetInstance.instanceData.firstElementChild,
|
|
535
|
+
this,
|
|
535
536
|
);
|
|
536
537
|
|
|
537
538
|
if (
|
|
538
|
-
|
|
539
|
-
|
|
539
|
+
this.responseMediatype === 'application/xml' ||
|
|
540
|
+
this.responseMediatype === 'text/html'
|
|
540
541
|
) {
|
|
541
542
|
const clone = data.firstElementChild;
|
|
542
543
|
const parent = theTarget.parentNode;
|
|
@@ -550,9 +551,9 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
550
551
|
}
|
|
551
552
|
} else if (this.into) {
|
|
552
553
|
const [theTarget] = evaluateXPath(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
554
|
+
this.into,
|
|
555
|
+
targetInstance.instanceData.firstElementChild,
|
|
556
|
+
this,
|
|
556
557
|
);
|
|
557
558
|
if (data?.nodeType === Node.DOCUMENT_NODE) {
|
|
558
559
|
theTarget.appendChild(data.firstElementChild);
|
|
@@ -571,9 +572,9 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
571
572
|
|
|
572
573
|
// ✅ treat instance replacement as a structural change
|
|
573
574
|
const fore =
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
575
|
+
(typeof this.getOwnerForm === 'function' && this.getOwnerForm()) ||
|
|
576
|
+
this.closest('fx-fore') ||
|
|
577
|
+
this.getModel()?.parentNode;
|
|
577
578
|
|
|
578
579
|
if (fore) {
|
|
579
580
|
fore.someInstanceDataStructureChanged = true;
|
|
@@ -589,10 +590,11 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
589
590
|
}
|
|
590
591
|
|
|
591
592
|
if (this.replace === 'download') {
|
|
592
|
-
|
|
593
|
+
let target = this._getProperty('target');
|
|
593
594
|
if (!target) {
|
|
594
595
|
throw new Error(`${this.id} needs to specify "target" attribute`);
|
|
595
596
|
}
|
|
597
|
+
target = this.evaluateAttributeTemplateExpression(target, this);
|
|
596
598
|
const downloadLink = document.createElement('a');
|
|
597
599
|
downloadLink.setAttribute('download', target);
|
|
598
600
|
downloadLink.setAttribute('href', `data:${contentType},${encodeURIComponent(data)}`);
|
|
@@ -602,7 +604,10 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
602
604
|
}
|
|
603
605
|
|
|
604
606
|
if (this.replace === 'all') {
|
|
605
|
-
|
|
607
|
+
let target = this._getProperty('target');
|
|
608
|
+
if (target) {
|
|
609
|
+
target = this.evaluateAttributeTemplateExpression(target, this);
|
|
610
|
+
}
|
|
606
611
|
if (target && target === '_blank') {
|
|
607
612
|
const win = window.open('', '_blank');
|
|
608
613
|
win.document.write(`<pre>${data}</pre>`);
|
|
@@ -617,7 +622,10 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
617
622
|
}
|
|
618
623
|
|
|
619
624
|
if (this.replace === 'target') {
|
|
620
|
-
|
|
625
|
+
let target = this._getProperty('target');
|
|
626
|
+
if (target) {
|
|
627
|
+
target = this.evaluateAttributeTemplateExpression(target, this);
|
|
628
|
+
}
|
|
621
629
|
const targetNode = document.querySelector(target);
|
|
622
630
|
if (targetNode) {
|
|
623
631
|
if (contentType && contentType.startsWith('text/html')) {
|
package/src/fx-var.js
CHANGED
|
@@ -17,7 +17,6 @@ export class FxVariable extends ForeElementMixin {
|
|
|
17
17
|
this.name = '';
|
|
18
18
|
this.valueQuery = '';
|
|
19
19
|
this.value = null;
|
|
20
|
-
this.precedingVariables = [];
|
|
21
20
|
// Re-entrancy guard for variable evaluation
|
|
22
21
|
this._isRefreshing = false;
|
|
23
22
|
// Cached typed value (Fonto sequence wrapper)
|
|
@@ -41,10 +40,11 @@ export class FxVariable extends ForeElementMixin {
|
|
|
41
40
|
|
|
42
41
|
const inscope = getInScopeContext(this, this.valueQuery);
|
|
43
42
|
|
|
44
|
-
// Evaluate
|
|
45
|
-
const values = evaluateXPath(this.valueQuery, inscope, this, this.
|
|
43
|
+
// Evaluate with the lexically scoped variables (preceding vars only), values read live
|
|
44
|
+
const values = evaluateXPath(this.valueQuery, inscope, this, this._precedingVariables());
|
|
46
45
|
|
|
47
46
|
// Cache typed value for other computations to consume without triggering evaluation
|
|
47
|
+
this._rawValues = values;
|
|
48
48
|
this._value = typedValueFactory(values, domFacade);
|
|
49
49
|
this.value = this._value;
|
|
50
50
|
} finally {
|
|
@@ -52,28 +52,64 @@ export class FxVariable extends ForeElementMixin {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Runs one evaluation point for this variable and reports whether its value changed.
|
|
57
|
+
*
|
|
58
|
+
* Comparison is shallow over the raw result sequence: node items by identity,
|
|
59
|
+
* atomic items by value. A node-valued variable whose nodes mutated content but kept
|
|
60
|
+
* identity is NOT flagged — variables hold node references, and consumers read the
|
|
61
|
+
* current content through them.
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean} true when the raw result sequence differs from the previous one
|
|
64
|
+
*/
|
|
65
|
+
refreshAndReportChange() {
|
|
66
|
+
const before = this._rawValues;
|
|
67
|
+
this.refresh();
|
|
68
|
+
const after = this._rawValues;
|
|
69
|
+
if (before === after) return false;
|
|
70
|
+
if (!Array.isArray(before) || !Array.isArray(after)) return true;
|
|
71
|
+
if (before.length !== after.length) return true;
|
|
72
|
+
return before.some((item, i) => item !== after[i]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Variables visible to this var per lexical scoping: the entries of the Map cloned at
|
|
77
|
+
* registration time (preceding vars only), with values read live at the evaluation point.
|
|
78
|
+
* The var itself is excluded — a variable is not visible to its own value expression.
|
|
79
|
+
*
|
|
80
|
+
* @returns {Object<string, *>}
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
_precedingVariables() {
|
|
84
|
+
const variables = {};
|
|
85
|
+
if (!this.inScopeVariables) return variables;
|
|
86
|
+
for (const [name, variableOrValue] of this.inScopeVariables) {
|
|
87
|
+
// eslint-disable-next-line no-continue
|
|
88
|
+
if (!variableOrValue || variableOrValue === this) continue;
|
|
89
|
+
if (variableOrValue.nodeType) {
|
|
90
|
+
// eslint-disable-next-line no-continue
|
|
91
|
+
if (variableOrValue._isRefreshing) continue;
|
|
92
|
+
variables[name] = variableOrValue._value ?? variableOrValue.value ?? null;
|
|
93
|
+
} else {
|
|
94
|
+
variables[name] = variableOrValue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return variables;
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/**
|
|
56
101
|
* @param {Map<string, FxVariable>} inScopeVariables
|
|
57
102
|
*/
|
|
58
103
|
setInScopeVariables(inScopeVariables) {
|
|
59
104
|
if (inScopeVariables.has(this.name)) {
|
|
60
105
|
console.error(`The variable ${this.name} is declared more than once`);
|
|
61
|
-
Fore.dispatch(this, '
|
|
106
|
+
Fore.dispatch(this, 'binding-error', {});
|
|
62
107
|
return;
|
|
63
108
|
}
|
|
64
109
|
inScopeVariables.set(this.name, this);
|
|
65
110
|
// Clone the preceding variables to make sure we are not going to get access to variables we
|
|
66
111
|
// should not get access to
|
|
67
112
|
this.inScopeVariables = new Map(inScopeVariables);
|
|
68
|
-
|
|
69
|
-
// Set precedingVariables based on inScopeVariables
|
|
70
|
-
this.precedingVariables = Array.from(inScopeVariables.entries()).map(([name, variable]) => {
|
|
71
|
-
// IMPORTANT: do not trigger evaluation while taking the snapshot
|
|
72
|
-
if (variable && variable._isRefreshing) {
|
|
73
|
-
return { name, value: null };
|
|
74
|
-
}
|
|
75
|
-
return { name, value: variable?._value ?? variable?.value ?? null };
|
|
76
|
-
});
|
|
77
113
|
}
|
|
78
114
|
}
|
|
79
115
|
if (!customElements.get('fx-var')) {
|
package/src/getInScopeContext.js
CHANGED
|
@@ -69,7 +69,7 @@ function _getInitialContext(node, ref) {
|
|
|
69
69
|
export default function getInScopeContext(node, ref) {
|
|
70
70
|
// console.log('getInScopeContext', ref, node);
|
|
71
71
|
|
|
72
|
-
//todo: check for multi-step pathes
|
|
72
|
+
// todo: check for multi-step pathes
|
|
73
73
|
const parentElement = _getElement(node);
|
|
74
74
|
// console.log('getInScopeContext parent', parentElement);
|
|
75
75
|
|
package/src/json/JSONLens.js
CHANGED
|
@@ -39,7 +39,7 @@ export class JSONLens {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
insert(value, keyOrIndex = null) {
|
|
42
|
-
const
|
|
42
|
+
const target = this.get();
|
|
43
43
|
|
|
44
44
|
if (Array.isArray(target)) {
|
|
45
45
|
if (keyOrIndex === null || keyOrIndex >= target.length) {
|
|
@@ -62,6 +62,6 @@ export class JSONLens {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
pathString() {
|
|
65
|
-
return
|
|
65
|
+
return `/${this.path.map(k => (typeof k === 'number' ? `[${k}]` : k)).join('/')}`;
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/ui/UIElement.js
CHANGED
|
@@ -43,10 +43,14 @@ export class UIElement extends ForeElementMixin {
|
|
|
43
43
|
|
|
44
44
|
disconnectedCallback() {
|
|
45
45
|
if (this.modelItem && typeof this.modelItem.removeObserver === 'function') {
|
|
46
|
-
console.log(`[UIElement] Removing observer for ref="${this.ref}"`);
|
|
47
46
|
this.modelItem.removeObserver(this);
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
this._refTrackedModelItems.forEach(modelItems => {
|
|
50
|
+
modelItems.forEach(mi => mi.removeObserver(this));
|
|
51
|
+
});
|
|
52
|
+
this._refTrackedModelItems.clear();
|
|
53
|
+
|
|
50
54
|
for (const removeEventListener of this._removeEventListeners) {
|
|
51
55
|
removeEventListener();
|
|
52
56
|
}
|
|
@@ -96,6 +100,15 @@ export class UIElement extends ForeElementMixin {
|
|
|
96
100
|
}
|
|
97
101
|
*/
|
|
98
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Mirrors relevant/nonrelevant into the `inert` primitive so a nonrelevant control is
|
|
105
|
+
* unreachable by keyboard/AT even if CSS visibility rules ever change. CSS already hides
|
|
106
|
+
* nonrelevant elements (display:none), so this is a semantic backstop, not the primary mechanism.
|
|
107
|
+
*/
|
|
108
|
+
_reflectRelevantInert(enabled) {
|
|
109
|
+
this.inert = !enabled;
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
attachObserver() {
|
|
100
113
|
const modelItem = this.getModelItem();
|
|
101
114
|
if (!modelItem || typeof modelItem.addObserver !== 'function') return;
|
|
@@ -120,7 +133,7 @@ export class UIElement extends ForeElementMixin {
|
|
|
120
133
|
* @param {import('../modelitem.js').ModelItem} modelItem - The ModelItem that changed
|
|
121
134
|
*/
|
|
122
135
|
|
|
123
|
-
/*
|
|
136
|
+
/*
|
|
124
137
|
update(modelItem) {
|
|
125
138
|
if (this.isBound()) {
|
|
126
139
|
// console.log('[UIElement] update()', modelItem);
|
|
@@ -146,16 +159,13 @@ export class UIElement extends ForeElementMixin {
|
|
|
146
159
|
// throw new Error('You have to implement the method init!');
|
|
147
160
|
// }
|
|
148
161
|
|
|
149
|
-
async refresh(force) {
|
|
150
|
-
console.log(`🔄 [UIElement] refresh() called for ref="${this.ref}"`);
|
|
151
|
-
}
|
|
162
|
+
async refresh(force) {}
|
|
152
163
|
|
|
153
164
|
async refreshChildren(force) {
|
|
154
165
|
await Fore.refreshChildren(this, force);
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
activate() {
|
|
158
|
-
console.log('UIElement.activate() called');
|
|
159
169
|
this.removeAttribute('on-demand');
|
|
160
170
|
this.style.display = '';
|
|
161
171
|
if (this.isBound()) {
|
|
@@ -198,12 +208,12 @@ export class UIElement extends ForeElementMixin {
|
|
|
198
208
|
icon.style.cursor = 'pointer';
|
|
199
209
|
icon.style.marginLeft = '0.5em';
|
|
200
210
|
|
|
201
|
-
icon.addEventListener('click', e => {
|
|
211
|
+
icon.addEventListener('click', async e => {
|
|
202
212
|
e.stopPropagation();
|
|
203
213
|
this.setAttribute('on-demand', 'true');
|
|
204
214
|
this.style.display = 'none';
|
|
205
215
|
document.dispatchEvent(new CustomEvent('update-control-menu'));
|
|
206
|
-
Fore.dispatch(this, 'hide-control', {});
|
|
216
|
+
await Fore.dispatch(this, 'hide-control', {});
|
|
207
217
|
});
|
|
208
218
|
|
|
209
219
|
this.appendChild(icon);
|
|
@@ -31,6 +31,8 @@ function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
|
|
|
31
31
|
* is a general base class for control elements.
|
|
32
32
|
*
|
|
33
33
|
*/
|
|
34
|
+
const NATIVE_FORM_TAGS = new Set(['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON']);
|
|
35
|
+
|
|
34
36
|
export default class AbstractControl extends UIElement {
|
|
35
37
|
constructor() {
|
|
36
38
|
super();
|
|
@@ -50,6 +52,31 @@ export default class AbstractControl extends UIElement {
|
|
|
50
52
|
throw new Error('You have to implement the method getWidget!');
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Native form elements (input/select/textarea/button) already expose required/readonly/disabled
|
|
57
|
+
* to assistive tech via their native attributes - mirroring those into aria-* would be redundant
|
|
58
|
+
* ARIA. Non-native widgets (custom elements used as `.widget`) have no such native mapping, so
|
|
59
|
+
* they need the explicit aria-* attributes.
|
|
60
|
+
*/
|
|
61
|
+
// eslint-disable-next-line class-methods-use-this
|
|
62
|
+
_isNativeFormWidget(widget) {
|
|
63
|
+
return !!widget && NATIVE_FORM_TAGS.has(widget.tagName);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Appends `id` to the widget's aria-describedby without clobbering any existing references
|
|
68
|
+
* (e.g. a hint that already contributed its id).
|
|
69
|
+
*/
|
|
70
|
+
_addDescribedBy(id) {
|
|
71
|
+
if (!id) return;
|
|
72
|
+
const widget = this.getWidget?.() || this.widget;
|
|
73
|
+
if (!widget) return;
|
|
74
|
+
const existing = (widget.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
75
|
+
if (!existing.includes(id)) {
|
|
76
|
+
widget.setAttribute('aria-describedby', [...existing, id].join(' '));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
/**
|
|
54
81
|
* (re)apply all modelItem state properties to this control. model -> UI
|
|
55
82
|
*/
|
|
@@ -232,6 +259,9 @@ export default class AbstractControl extends UIElement {
|
|
|
232
259
|
if (!this.modelItem.required) {
|
|
233
260
|
this.widget.removeAttribute('required');
|
|
234
261
|
this.removeAttribute('required');
|
|
262
|
+
if (!this._isNativeFormWidget(this.widget)) {
|
|
263
|
+
this.widget.removeAttribute('aria-required');
|
|
264
|
+
}
|
|
235
265
|
if (wasRequired !== this.modelItem.required) {
|
|
236
266
|
this._dispatchEvent('optional');
|
|
237
267
|
}
|
|
@@ -250,6 +280,9 @@ export default class AbstractControl extends UIElement {
|
|
|
250
280
|
}
|
|
251
281
|
this.widget.setAttribute('required', '');
|
|
252
282
|
this.setAttribute('required', '');
|
|
283
|
+
if (!this._isNativeFormWidget(this.widget)) {
|
|
284
|
+
this.widget.setAttribute('aria-required', 'true');
|
|
285
|
+
}
|
|
253
286
|
if (wasRequired !== this.modelItem.required) {
|
|
254
287
|
this._dispatchEvent('required');
|
|
255
288
|
}
|
|
@@ -325,10 +358,16 @@ export default class AbstractControl extends UIElement {
|
|
|
325
358
|
if (effectiveReadonly) {
|
|
326
359
|
this.widget.setAttribute('readonly', '');
|
|
327
360
|
this.setAttribute('readonly', '');
|
|
361
|
+
if (!this._isNativeFormWidget(this.widget)) {
|
|
362
|
+
this.widget.setAttribute('aria-readonly', 'true');
|
|
363
|
+
}
|
|
328
364
|
this._dispatchEvent('readonly');
|
|
329
365
|
} else {
|
|
330
366
|
this.widget.removeAttribute('readonly');
|
|
331
367
|
this.removeAttribute('readonly');
|
|
368
|
+
if (!this._isNativeFormWidget(this.widget)) {
|
|
369
|
+
this.widget.removeAttribute('aria-readonly');
|
|
370
|
+
}
|
|
332
371
|
this._dispatchEvent('readwrite');
|
|
333
372
|
}
|
|
334
373
|
}
|
|
@@ -346,7 +385,6 @@ export default class AbstractControl extends UIElement {
|
|
|
346
385
|
}
|
|
347
386
|
|
|
348
387
|
// todo - review alert handling altogether. There could be potentially multiple ones in model
|
|
349
|
-
// TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code
|
|
350
388
|
handleValid() {
|
|
351
389
|
// console.log('mip valid', this.modelItem.required);
|
|
352
390
|
|
|
@@ -363,12 +401,10 @@ export default class AbstractControl extends UIElement {
|
|
|
363
401
|
this._dispatchEvent('valid');
|
|
364
402
|
this.setAttribute('valid', '');
|
|
365
403
|
this.removeAttribute('invalid');
|
|
366
|
-
this.getWidget().setAttribute('aria-invalid', 'false');
|
|
367
404
|
// also reset other dependent CSS classes
|
|
368
405
|
this.classList.remove('isEmpty');
|
|
369
406
|
} else {
|
|
370
407
|
this.setAttribute('invalid', '');
|
|
371
|
-
this.getWidget().setAttribute('aria-invalid', 'true');
|
|
372
408
|
this.removeAttribute('valid');
|
|
373
409
|
// ### constraint is invalid - handle alerts
|
|
374
410
|
/*
|
|
@@ -384,10 +420,15 @@ export default class AbstractControl extends UIElement {
|
|
|
384
420
|
alerts.forEach(modelAlert => {
|
|
385
421
|
const newAlert = document.createElement('fx-alert');
|
|
386
422
|
// const newAlert = document.createElement('span');
|
|
423
|
+
newAlert.id = newAlert.id || `fx-alert-${Fore.createUUID()}`;
|
|
387
424
|
newAlert.innerHTML = modelAlert;
|
|
388
425
|
this.appendChild(newAlert);
|
|
426
|
+
this._addDescribedBy(newAlert.id);
|
|
389
427
|
// newAlert.style.display = 'block';
|
|
390
428
|
});
|
|
429
|
+
} else {
|
|
430
|
+
controlAlert.id = controlAlert.id || `fx-alert-${Fore.createUUID()}`;
|
|
431
|
+
this._addDescribedBy(controlAlert.id);
|
|
391
432
|
}
|
|
392
433
|
}
|
|
393
434
|
|
|
@@ -425,6 +466,7 @@ export default class AbstractControl extends UIElement {
|
|
|
425
466
|
this.setAttribute('nonrelevant', '');
|
|
426
467
|
this.removeAttribute('relevant');
|
|
427
468
|
}
|
|
469
|
+
this._reflectRelevantInert(newEnabled);
|
|
428
470
|
|
|
429
471
|
// Dispatch only on actual change
|
|
430
472
|
if (wasEnabled !== newEnabled) {
|
package/src/ui/fx-alert.js
CHANGED
package/src/ui/fx-case.js
CHANGED
|
@@ -93,7 +93,7 @@ export class FxCase extends FxContainer {
|
|
|
93
93
|
ownerForm.addToBatchedNotifications(target);
|
|
94
94
|
ownerForm.refresh(false);
|
|
95
95
|
});
|
|
96
|
-
this.addEventListener('deselect',
|
|
96
|
+
this.addEventListener('deselect', event => {
|
|
97
97
|
const ownerForm = this.getOwnerForm();
|
|
98
98
|
ownerForm.addToBatchedNotifications(event.target);
|
|
99
99
|
});
|
package/src/ui/fx-container.js
CHANGED
|
@@ -113,6 +113,7 @@ export class FxContainer extends UIElement {
|
|
|
113
113
|
// console.log('container is not relevant');
|
|
114
114
|
this.removeAttribute('relevant', '');
|
|
115
115
|
this.setAttribute('nonrelevant', '');
|
|
116
|
+
this._reflectRelevantInert(false);
|
|
116
117
|
this.dispatchEvent(new CustomEvent('disabled', {}));
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
@@ -122,10 +123,12 @@ export class FxContainer extends UIElement {
|
|
|
122
123
|
// this.style.display = 'block';
|
|
123
124
|
this.removeAttribute('nonrelevant', '');
|
|
124
125
|
this.setAttribute('relevant', '');
|
|
126
|
+
this._reflectRelevantInert(true);
|
|
125
127
|
this.dispatchEvent(new CustomEvent('enabled', {}));
|
|
126
128
|
} else {
|
|
127
129
|
this.removeAttribute('relevant', '');
|
|
128
130
|
this.setAttribute('nonrelevant', '');
|
|
131
|
+
this._reflectRelevantInert(false);
|
|
129
132
|
this.dispatchEvent(new CustomEvent('disabled', {}));
|
|
130
133
|
}
|
|
131
134
|
}
|