@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
|
@@ -5,12 +5,15 @@ import XfAbstractControl from './abstract-control.js';
|
|
|
5
5
|
* in a popup list for activation. 'on-demand' is not a state like 'relevant' but just
|
|
6
6
|
* shows/hides controls on demand. The controls still behave as usual otherwise.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Implements the ARIA "menu button" pattern: the slotted trigger gets `aria-haspopup`/
|
|
9
|
+
* `aria-expanded`/`aria-controls`, the popup gets `role="menu"` with `role="menuitem"`
|
|
10
|
+
* entries, and Up/Down/Home/End move a roving tabindex across the open menu.
|
|
9
11
|
*/
|
|
10
12
|
export class FxControlMenu extends XfAbstractControl {
|
|
11
13
|
connectedCallback() {
|
|
12
14
|
this.attachShadow({ mode: 'open' });
|
|
13
15
|
this.selectExpr = this.getAttribute('select');
|
|
16
|
+
this.triggerButton = null;
|
|
14
17
|
|
|
15
18
|
const style = `
|
|
16
19
|
:host {
|
|
@@ -52,10 +55,11 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
52
55
|
this.shadowRoot.innerHTML = `
|
|
53
56
|
<style>${style}</style>
|
|
54
57
|
<slot></slot>
|
|
55
|
-
<div class="menu" part="menu"></div>
|
|
58
|
+
<div class="menu" part="menu" role="menu"></div>
|
|
56
59
|
`;
|
|
57
60
|
|
|
58
61
|
this.menuEl = this.shadowRoot.querySelector('.menu');
|
|
62
|
+
this.menuEl.addEventListener('keydown', e => this._handleMenuKeydown(e));
|
|
59
63
|
|
|
60
64
|
// Slotted button click
|
|
61
65
|
const slot = this.shadowRoot.querySelector('slot');
|
|
@@ -64,12 +68,19 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
64
68
|
const button = nodes.find(
|
|
65
69
|
node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON',
|
|
66
70
|
);
|
|
67
|
-
if (button) {
|
|
71
|
+
if (button && button !== this.triggerButton) {
|
|
72
|
+
this.triggerButton = button;
|
|
73
|
+
button.setAttribute('aria-haspopup', 'true');
|
|
74
|
+
button.setAttribute('aria-expanded', 'false');
|
|
68
75
|
button.addEventListener('click', e => {
|
|
69
76
|
e.preventDefault();
|
|
70
77
|
e.stopPropagation();
|
|
71
78
|
this.updateMenu();
|
|
72
|
-
this.menuEl.classList.
|
|
79
|
+
if (this.menuEl.classList.contains('visible')) {
|
|
80
|
+
this._closeMenu();
|
|
81
|
+
} else {
|
|
82
|
+
this._openMenu();
|
|
83
|
+
}
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
86
|
});
|
|
@@ -83,14 +94,14 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
83
94
|
document.addEventListener('click', e => {
|
|
84
95
|
const inside = this.contains(e.target) || this.shadowRoot.contains(e.target);
|
|
85
96
|
if (!inside) {
|
|
86
|
-
this.
|
|
97
|
+
this._closeMenu();
|
|
87
98
|
}
|
|
88
99
|
});
|
|
89
100
|
|
|
90
|
-
// Close on Escape
|
|
101
|
+
// Close on Escape, returning focus to the trigger
|
|
91
102
|
document.addEventListener('keydown', e => {
|
|
92
|
-
if (e.key === 'Escape') {
|
|
93
|
-
this.
|
|
103
|
+
if (e.key === 'Escape' && this.menuEl.classList.contains('visible')) {
|
|
104
|
+
this._closeMenu({ restoreFocus: true });
|
|
94
105
|
}
|
|
95
106
|
});
|
|
96
107
|
|
|
@@ -132,11 +143,66 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
132
143
|
return null;
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Opens the popup, reflects `aria-expanded` on the trigger, and moves focus to the
|
|
148
|
+
* first menu item (ARIA "menu button" pattern).
|
|
149
|
+
*/
|
|
150
|
+
_openMenu() {
|
|
151
|
+
this.menuEl.classList.add('visible');
|
|
152
|
+
this.triggerButton?.setAttribute('aria-expanded', 'true');
|
|
153
|
+
this.menuEl.querySelector('a')?.focus();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Closes the popup and reflects `aria-expanded` on the trigger. Focus is only restored
|
|
158
|
+
* to the trigger for dismissal paths (Escape, outside click) - not on item selection,
|
|
159
|
+
* where `el.activate()` already moves focus to the newly revealed control.
|
|
160
|
+
*/
|
|
161
|
+
_closeMenu({ restoreFocus = false } = {}) {
|
|
162
|
+
this.menuEl.classList.remove('visible');
|
|
163
|
+
this.triggerButton?.setAttribute('aria-expanded', 'false');
|
|
164
|
+
if (restoreFocus) {
|
|
165
|
+
this.triggerButton?.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Roving-tabindex arrow-key navigation between menu items (Up/Down/Home/End), following
|
|
171
|
+
* the WAI-ARIA menu pattern.
|
|
172
|
+
*/
|
|
173
|
+
_handleMenuKeydown(e) {
|
|
174
|
+
const items = Array.from(this.menuEl.querySelectorAll('a'));
|
|
175
|
+
if (items.length === 0) return;
|
|
176
|
+
const idx = items.indexOf(e.target);
|
|
177
|
+
if (idx === -1) return;
|
|
178
|
+
|
|
179
|
+
let newIdx;
|
|
180
|
+
switch (e.key) {
|
|
181
|
+
case 'ArrowDown':
|
|
182
|
+
newIdx = (idx + 1) % items.length;
|
|
183
|
+
break;
|
|
184
|
+
case 'ArrowUp':
|
|
185
|
+
newIdx = (idx - 1 + items.length) % items.length;
|
|
186
|
+
break;
|
|
187
|
+
case 'Home':
|
|
188
|
+
newIdx = 0;
|
|
189
|
+
break;
|
|
190
|
+
case 'End':
|
|
191
|
+
newIdx = items.length - 1;
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
items.forEach((item, i) => item.setAttribute('tabindex', i === newIdx ? '0' : '-1'));
|
|
198
|
+
items[newIdx].focus();
|
|
199
|
+
}
|
|
200
|
+
|
|
135
201
|
updateMenu() {
|
|
136
202
|
const container = this._getScopedContainer();
|
|
137
203
|
if (!container) return;
|
|
138
204
|
|
|
139
|
-
|
|
205
|
+
const targets = [];
|
|
140
206
|
// ✅ Include container itself if it has on-demand
|
|
141
207
|
if (container.hasAttribute('on-demand')) {
|
|
142
208
|
targets.push(container);
|
|
@@ -163,7 +229,7 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
163
229
|
}
|
|
164
230
|
|
|
165
231
|
if (targets.length === 0) {
|
|
166
|
-
this.
|
|
232
|
+
this._closeMenu();
|
|
167
233
|
return;
|
|
168
234
|
}
|
|
169
235
|
|
|
@@ -182,6 +248,8 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
182
248
|
const item = document.createElement('a');
|
|
183
249
|
item.href = '#';
|
|
184
250
|
item.textContent = label;
|
|
251
|
+
item.setAttribute('role', 'menuitem');
|
|
252
|
+
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|
185
253
|
|
|
186
254
|
item.addEventListener('click', e => {
|
|
187
255
|
e.preventDefault();
|
|
@@ -189,7 +257,7 @@ export class FxControlMenu extends XfAbstractControl {
|
|
|
189
257
|
el.activate();
|
|
190
258
|
}
|
|
191
259
|
|
|
192
|
-
this.
|
|
260
|
+
this._closeMenu();
|
|
193
261
|
|
|
194
262
|
// Wait one frame to let DOM updates (like on-demand removal) take effect
|
|
195
263
|
requestAnimationFrame(() => {
|
package/src/ui/fx-control.js
CHANGED
|
@@ -9,8 +9,8 @@ import getInScopeContext from '../getInScopeContext.js';
|
|
|
9
9
|
import { Fore } from '../fore.js';
|
|
10
10
|
import { debounce } from '../events.js';
|
|
11
11
|
import { FxModel } from '../fx-model.js';
|
|
12
|
+
import { FxFore } from '../fx-fore.js';
|
|
12
13
|
import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
|
|
13
|
-
import { extractPredicateDependencies } from '../extract-predicate-deps.js';
|
|
14
14
|
import '../fx-instance.js';
|
|
15
15
|
|
|
16
16
|
const WIDGETCLASS = 'widget';
|
|
@@ -122,14 +122,23 @@ export default class FxControl extends XfAbstractControl {
|
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
this.
|
|
125
|
+
const controlHtml = this.renderHTML(this.ref);
|
|
126
|
+
const sheet = Fore.getSharedStyleSheet(style);
|
|
127
|
+
if (sheet) {
|
|
128
|
+
this.shadowRoot.innerHTML = controlHtml;
|
|
129
|
+
this.shadowRoot.adoptedStyleSheets = [sheet];
|
|
130
|
+
} else {
|
|
131
|
+
this.shadowRoot.innerHTML = `
|
|
126
132
|
<style>
|
|
127
133
|
${style}
|
|
128
134
|
</style>
|
|
129
|
-
${
|
|
135
|
+
${controlHtml}
|
|
130
136
|
`;
|
|
137
|
+
}
|
|
131
138
|
|
|
132
139
|
this.widget = this.getWidget();
|
|
140
|
+
this._associateLabel();
|
|
141
|
+
this._associateDescriptions();
|
|
133
142
|
|
|
134
143
|
this.addEventListener('mousedown', e => {
|
|
135
144
|
// ### prevent mousedown events on all control content that is not the widget or within the widget
|
|
@@ -155,6 +164,8 @@ export default class FxControl extends XfAbstractControl {
|
|
|
155
164
|
listenOn = target;
|
|
156
165
|
}
|
|
157
166
|
}
|
|
167
|
+
// retained for setValue()'s undo-coalescing focus check (see there)
|
|
168
|
+
this._listenOn = listenOn;
|
|
158
169
|
|
|
159
170
|
this.addEventListener('keyup', () => {
|
|
160
171
|
FxModel.dataChanged = true;
|
|
@@ -172,6 +183,14 @@ export default class FxControl extends XfAbstractControl {
|
|
|
172
183
|
});
|
|
173
184
|
this.updateEvent = 'blur'; // needs to be registered too
|
|
174
185
|
}
|
|
186
|
+
// blur both commits the final value and closes any open undo-coalescing session, so
|
|
187
|
+
// typing into this field again after leaving it starts a new undo step. Must be a
|
|
188
|
+
// normal (non-`once`) listener: a field can be focused/blurred more than once.
|
|
189
|
+
const onBlur = () => {
|
|
190
|
+
this.setValue(this._getValueOfWidget());
|
|
191
|
+
this.getModel()?.getEffectiveUndoManager()?.endCoalescingSession();
|
|
192
|
+
};
|
|
193
|
+
|
|
175
194
|
if (this.debounceDelay) {
|
|
176
195
|
listenOn.addEventListener(
|
|
177
196
|
this.updateEvent,
|
|
@@ -185,6 +204,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
185
204
|
this.debounceDelay,
|
|
186
205
|
),
|
|
187
206
|
);
|
|
207
|
+
listenOn.addEventListener('blur', onBlur);
|
|
188
208
|
} else {
|
|
189
209
|
listenOn.addEventListener(this.updateEvent, event => {
|
|
190
210
|
if (this._isRefreshing) {
|
|
@@ -193,13 +213,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
193
213
|
}
|
|
194
214
|
this.setValue(this._getValueOfWidget());
|
|
195
215
|
});
|
|
196
|
-
listenOn.addEventListener(
|
|
197
|
-
'blur',
|
|
198
|
-
event => {
|
|
199
|
-
this.setValue(this._getValueOfWidget());
|
|
200
|
-
},
|
|
201
|
-
{ once: true },
|
|
202
|
-
);
|
|
216
|
+
listenOn.addEventListener('blur', onBlur);
|
|
203
217
|
}
|
|
204
218
|
|
|
205
219
|
this.addEventListener('return', e => {
|
|
@@ -272,18 +286,61 @@ export default class FxControl extends XfAbstractControl {
|
|
|
272
286
|
return; // do nothing when modelItem is readonly
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
// direct widget edits bypass the action pipeline (no execute()), so undo capture
|
|
290
|
+
// happens here - unless an action chain is already running and capturing, or the
|
|
291
|
+
// form is mid-refresh (teardown blurs replay stale widget values; recording those
|
|
292
|
+
// would clear the redo stack right after an undo)
|
|
293
|
+
const undoManager = this.getModel()?.getEffectiveUndoManager();
|
|
294
|
+
const captureUndo =
|
|
295
|
+
!!undoManager?.enabled &&
|
|
296
|
+
FxFore.outermostHandler === null &&
|
|
297
|
+
!this.getOwnerForm()?.isRefreshing;
|
|
298
|
+
if (captureUndo) undoManager.beginCapture();
|
|
299
|
+
const undoKey = captureUndo
|
|
300
|
+
? Array.isArray(modelitem?.node)
|
|
301
|
+
? modelitem.node[0]
|
|
302
|
+
: modelitem?.node
|
|
303
|
+
: null;
|
|
304
|
+
const valueBefore = captureUndo ? modelitem?.value : null;
|
|
305
|
+
// coalesce only while the widget is genuinely focused - a session opened by a fully
|
|
306
|
+
// programmatic call (no real focus) would never see a blur to close it, and could
|
|
307
|
+
// silently absorb an unrelated later edit to the same node. Also coalesce when a
|
|
308
|
+
// session for this exact node is already open: the blur handler's own setValue() call
|
|
309
|
+
// fires *after* document.activeElement has already moved away, so without this it
|
|
310
|
+
// would wrongly split the final keystroke into its own entry instead of merging it
|
|
311
|
+
// into the session that blur is about to close.
|
|
312
|
+
const isFocused =
|
|
313
|
+
captureUndo &&
|
|
314
|
+
(document.activeElement === this._listenOn || undoManager?.openSession?.key === undoKey);
|
|
315
|
+
const commitUndo = () => {
|
|
316
|
+
if (!captureUndo) return;
|
|
317
|
+
if (modelitem && modelitem.value !== valueBefore) {
|
|
318
|
+
if (isFocused) {
|
|
319
|
+
undoManager.commitCoalesced(undoKey);
|
|
320
|
+
} else {
|
|
321
|
+
undoManager.commit(undoKey);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
undoManager.discard();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
275
328
|
if (this.getAttribute('as') === 'node') {
|
|
276
329
|
const replace = this.shadowRoot.getElementById('replace');
|
|
277
330
|
replace.replace(this.nodeset, val);
|
|
278
331
|
if (modelitem && val && val !== modelitem.value) {
|
|
279
332
|
modelitem.value = val;
|
|
280
333
|
FxModel.dataChanged = true;
|
|
334
|
+
commitUndo();
|
|
281
335
|
replace.actionPerformed();
|
|
336
|
+
} else {
|
|
337
|
+
commitUndo();
|
|
282
338
|
}
|
|
283
339
|
return;
|
|
284
340
|
}
|
|
285
341
|
const setval = this.shadowRoot.getElementById('setvalue');
|
|
286
342
|
setval.setValue(modelitem, val);
|
|
343
|
+
commitUndo();
|
|
287
344
|
|
|
288
345
|
/*
|
|
289
346
|
if (this.modelItem instanceof ModelItem && !this.modelItem?.boundControls.includes(this)) {
|
|
@@ -329,6 +386,41 @@ export default class FxControl extends XfAbstractControl {
|
|
|
329
386
|
`;
|
|
330
387
|
}
|
|
331
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Associates the control's label with its widget.
|
|
391
|
+
*
|
|
392
|
+
* Two label mechanisms exist: a light-DOM `<label>` child (slotted alongside the widget, so
|
|
393
|
+
* both share the same tree and `for`/`id` works), and the `label="..."` attribute (rendered as
|
|
394
|
+
* bare text into the *shadow root* by `renderHTML()` - a different tree than the light-DOM
|
|
395
|
+
* widget, so an id reference cannot cross that boundary and `aria-label` (a string, not an id
|
|
396
|
+
* reference) is used instead).
|
|
397
|
+
*/
|
|
398
|
+
_associateLabel() {
|
|
399
|
+
const label = this.querySelector(':scope > label');
|
|
400
|
+
if (label) {
|
|
401
|
+
const id = label.getAttribute('for') || this.widget.id || `fx-${Fore.createUUID()}`;
|
|
402
|
+
if (!label.hasAttribute('for')) label.setAttribute('for', id);
|
|
403
|
+
if (!this.widget.id) this.widget.id = id;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this.label) {
|
|
407
|
+
this.widget.setAttribute('aria-label', this.label);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Wires any statically-authored `<fx-hint>`/`<fx-alert>` light-DOM children into the widget's
|
|
413
|
+
* aria-describedby, so validation/help text is announced as part of the field's description.
|
|
414
|
+
* Dynamically-created alerts (from a `<fx-bind>`'s `alert`) are wired separately in
|
|
415
|
+
* `AbstractControl.handleValid()`.
|
|
416
|
+
*/
|
|
417
|
+
_associateDescriptions() {
|
|
418
|
+
this.querySelectorAll(':scope > fx-hint, :scope > fx-alert').forEach(el => {
|
|
419
|
+
el.id = el.id || `fx-${el.localName}-${Fore.createUUID()}`;
|
|
420
|
+
this._addDescribedBy(el.id);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
332
424
|
/**
|
|
333
425
|
* The widget is the actual control being used in the UI e.g. a native input control or any
|
|
334
426
|
* other component that presents a control that can be interacted with.
|
|
@@ -551,13 +643,11 @@ export default class FxControl extends XfAbstractControl {
|
|
|
551
643
|
this.widget = imported;
|
|
552
644
|
|
|
553
645
|
if (!theFore) {
|
|
554
|
-
Fore.dispatch('error', {
|
|
555
|
-
|
|
556
|
-
message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
|
|
557
|
-
},
|
|
646
|
+
await Fore.dispatch(this, 'error', {
|
|
647
|
+
message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
|
|
558
648
|
});
|
|
559
649
|
}
|
|
560
|
-
Fore.dispatch('loaded', {
|
|
650
|
+
await Fore.dispatch(this, 'loaded', { fore: theFore });
|
|
561
651
|
} catch (error) {
|
|
562
652
|
// console.log('error', error);
|
|
563
653
|
Fore.dispatch(this, 'error', {
|
|
@@ -689,7 +779,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
689
779
|
if (dataRefd && dataRefd.closest('fx-control') === this) {
|
|
690
780
|
this.boundList = dataRefd;
|
|
691
781
|
const ref = dataRefd.getAttribute('data-ref');
|
|
692
|
-
this._handleBoundWidget(dataRefd, true); //todo: revisit !!! observer
|
|
782
|
+
this._handleBoundWidget(dataRefd, true); // todo: revisit !!! observer
|
|
693
783
|
}
|
|
694
784
|
}
|
|
695
785
|
|
|
@@ -714,23 +804,18 @@ export default class FxControl extends XfAbstractControl {
|
|
|
714
804
|
const inscope = getInScopeContext(this, ref);
|
|
715
805
|
// const nodeset = evaluateXPathToNodes(ref, inscope, this);
|
|
716
806
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
ref,
|
|
730
|
-
model,
|
|
731
|
-
mi => mi.addObserver(this),
|
|
732
|
-
node => model.getModelItem(node),
|
|
733
|
-
);
|
|
807
|
+
let touchedNodes = null;
|
|
808
|
+
let domFacade = null;
|
|
809
|
+
if (this._refNeedsDependencyTracking(ref)) {
|
|
810
|
+
touchedNodes = new Set();
|
|
811
|
+
domFacade = new DependencyNotifyingDomFacade(node => touchedNodes.add(node));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const nodeset = evaluateXPath(ref, inscope, this, {}, {}, domFacade);
|
|
815
|
+
|
|
816
|
+
if (touchedNodes) {
|
|
817
|
+
this._trackRefDependencies(touchedNodes, 'widget');
|
|
818
|
+
}
|
|
734
819
|
|
|
735
820
|
// ### bail out when nodeset is array and empty
|
|
736
821
|
if (Array.isArray(nodeset) && nodeset.length === 0) return;
|
|
@@ -783,6 +868,12 @@ export default class FxControl extends XfAbstractControl {
|
|
|
783
868
|
this.updateEntry(newEntry, nodeset);
|
|
784
869
|
}
|
|
785
870
|
this.boundInitialized = true;
|
|
871
|
+
|
|
872
|
+
// Newly created entries may carry template expressions in attributes that
|
|
873
|
+
// updateEntry() does not know about (e.g. a `name="{$qno}"` on a generated
|
|
874
|
+
// radio input). Flag them for scanning on the next fx-fore refresh, the same
|
|
875
|
+
// way fx-repeat does for newly rendered repeat items.
|
|
876
|
+
this.getOwnerForm()?.scanForNewTemplateExpressionsNextRefresh();
|
|
786
877
|
}
|
|
787
878
|
|
|
788
879
|
if (this.valueProp === 'selectedOptions') {
|
|
@@ -809,8 +900,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
809
900
|
}
|
|
810
901
|
|
|
811
902
|
const valueExpr = valueAttribute;
|
|
812
|
-
const
|
|
813
|
-
const evaluated = evaluateXPathToString(cutted, node, this);
|
|
903
|
+
const evaluated = Fore.evaluateTemplateString(valueExpr, node, this);
|
|
814
904
|
newEntry.setAttribute('value', evaluated);
|
|
815
905
|
|
|
816
906
|
if (this.value === evaluated) {
|
|
@@ -818,10 +908,9 @@ export default class FxControl extends XfAbstractControl {
|
|
|
818
908
|
}
|
|
819
909
|
|
|
820
910
|
if (newEntry.hasAttribute('title')) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
newEntry.setAttribute('title', evaluated);
|
|
911
|
+
const titleExpr = newEntry.getAttribute('title');
|
|
912
|
+
const evaluatedTitle = Fore.evaluateTemplateString(titleExpr, node, newEntry);
|
|
913
|
+
newEntry.setAttribute('title', evaluatedTitle);
|
|
825
914
|
}
|
|
826
915
|
// ### set label
|
|
827
916
|
const optionLabel = newEntry.textContent.trim();
|
|
@@ -836,10 +925,10 @@ export default class FxControl extends XfAbstractControl {
|
|
|
836
925
|
* @param {HTMLElement} newEntry
|
|
837
926
|
*/
|
|
838
927
|
evalLabel(optionLabel, node, newEntry) {
|
|
839
|
-
const labelExpr = optionLabel.trim()
|
|
928
|
+
const labelExpr = optionLabel.trim();
|
|
840
929
|
if (!labelExpr) return;
|
|
841
930
|
|
|
842
|
-
const label =
|
|
931
|
+
const label = Fore.evaluateTemplateString(labelExpr, node, this);
|
|
843
932
|
newEntry.textContent = label;
|
|
844
933
|
}
|
|
845
934
|
|
package/src/ui/fx-dialog.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Fore } from '../fore.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated - use the native HTML `<dialog>` element instead, which already provides a
|
|
5
|
+
* focus trap, `aria-modal`, and Escape-to-close for free. `fx-show`/`fx-hide` work unchanged
|
|
6
|
+
* against `<dialog>` (see `demo/dialog.html`), so this is a markup-only migration.
|
|
7
|
+
*/
|
|
3
8
|
export class FxDialog extends HTMLElement {
|
|
4
9
|
static get properties() {
|
|
5
10
|
return {
|
package/src/ui/fx-items.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveId } from '../xpath-evaluation.js';
|
|
2
2
|
import FxControl from './fx-control.js';
|
|
3
3
|
import { Fore } from '../fore.js';
|
|
4
4
|
import { XPathUtil } from '../xpath-util.js';
|
|
@@ -119,11 +119,12 @@ export class FxItems extends FxControl {
|
|
|
119
119
|
|
|
120
120
|
// ### handle 'label'
|
|
121
121
|
const label = newEntry.querySelector('label');
|
|
122
|
-
const lblExpr = Fore.getExpression(label.textContent);
|
|
123
122
|
|
|
124
123
|
// ROOT FIX: JSON lens nodes are objects; do NOT use direct JS property access.
|
|
125
124
|
// Always go through evaluateXPathToString() for JSON too.
|
|
126
|
-
|
|
125
|
+
// Use evaluateTemplateString() rather than getExpression() as the label may mix
|
|
126
|
+
// static text with one or more `{expr}` placeholders (e.g. "foo {name}").
|
|
127
|
+
const lblEvaluated = Fore.evaluateTemplateString(label.textContent, node, this);
|
|
127
128
|
// console.log('lblEvaluated ', lblEvaluated);
|
|
128
129
|
label.textContent = lblEvaluated;
|
|
129
130
|
|
|
@@ -134,10 +135,9 @@ export class FxItems extends FxControl {
|
|
|
134
135
|
const input = newEntry.querySelector('[value]');
|
|
135
136
|
// getting expr
|
|
136
137
|
const expr = input.value;
|
|
137
|
-
const cutted = Fore.getExpression(expr);
|
|
138
138
|
|
|
139
139
|
// ROOT FIX: same here
|
|
140
|
-
const evaluated =
|
|
140
|
+
const evaluated = Fore.evaluateTemplateString(expr, node, this);
|
|
141
141
|
// console.log('evaluated ', lblEvaluated);
|
|
142
142
|
|
|
143
143
|
// Set both property and attribute so *any* downstream code path works
|
|
@@ -158,4 +158,4 @@ export class FxItems extends FxControl {
|
|
|
158
158
|
|
|
159
159
|
if (!customElements.get('fx-items')) {
|
|
160
160
|
customElements.define('fx-items', FxItems);
|
|
161
|
-
}
|
|
161
|
+
}
|
package/src/ui/fx-output.js
CHANGED
|
@@ -51,17 +51,39 @@ export class FxOutput extends XfAbstractControl {
|
|
|
51
51
|
</span>
|
|
52
52
|
`;
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
const sheet = Fore.getSharedStyleSheet(style);
|
|
55
|
+
if (sheet) {
|
|
56
|
+
this.shadowRoot.innerHTML = outputHtml;
|
|
57
|
+
this.shadowRoot.adoptedStyleSheets = [sheet];
|
|
58
|
+
} else {
|
|
59
|
+
this.shadowRoot.innerHTML = `
|
|
55
60
|
<style>
|
|
56
61
|
${style}
|
|
57
62
|
</style>
|
|
58
63
|
${outputHtml}
|
|
59
64
|
`;
|
|
65
|
+
}
|
|
60
66
|
// this.widget = this.shadowRoot.querySelector('#widget');
|
|
61
67
|
// this.widget = this.getWidget();
|
|
62
68
|
// console.log('widget ', this.widget);
|
|
63
69
|
this.mediatype = this.hasAttribute('mediatype') ? this.getAttribute('mediatype') : null;
|
|
64
70
|
|
|
71
|
+
// The label is a slotted light-DOM element while the widget is a shadow-DOM <span> - an id
|
|
72
|
+
// reference (for/aria-labelledby) can't cross that boundary, so derive an aria-label string
|
|
73
|
+
// (which can) from the slotted label's text instead.
|
|
74
|
+
const labelSlot = this.shadowRoot.querySelector('slot[name="label"]');
|
|
75
|
+
const applyLabel = () => {
|
|
76
|
+
const text = this._getLabelText();
|
|
77
|
+
const valueEl = this.getWidget();
|
|
78
|
+
if (text) {
|
|
79
|
+
valueEl.setAttribute('aria-label', text);
|
|
80
|
+
} else {
|
|
81
|
+
valueEl.removeAttribute('aria-label');
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
labelSlot.addEventListener('slotchange', applyLabel);
|
|
85
|
+
applyLabel();
|
|
86
|
+
|
|
65
87
|
/*
|
|
66
88
|
this.addEventListener('slotchange', e => {
|
|
67
89
|
console.log('slotchange ', e);
|
|
@@ -103,6 +125,16 @@ export class FxOutput extends XfAbstractControl {
|
|
|
103
125
|
return valueWrapper;
|
|
104
126
|
}
|
|
105
127
|
|
|
128
|
+
_getLabelText() {
|
|
129
|
+
const labelSlot = this.shadowRoot.querySelector('slot[name="label"]');
|
|
130
|
+
if (!labelSlot) return '';
|
|
131
|
+
return labelSlot
|
|
132
|
+
.assignedNodes({ flatten: true })
|
|
133
|
+
.map(n => n.textContent)
|
|
134
|
+
.join('')
|
|
135
|
+
.trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
handleReadonly() {
|
|
107
139
|
// An output is always read-only
|
|
108
140
|
this.setAttribute('readonly', 'readonly');
|
|
@@ -138,6 +170,10 @@ export class FxOutput extends XfAbstractControl {
|
|
|
138
170
|
if (this.mediatype === 'image') {
|
|
139
171
|
const img = document.createElement('img');
|
|
140
172
|
img.setAttribute('src', this.value);
|
|
173
|
+
// axe/WCAG require the accessible name directly on the <img> - aria-label on the
|
|
174
|
+
// wrapping #value span (see applyLabel() above) doesn't satisfy that for a descendant
|
|
175
|
+
// img. Empty alt is still valid (marks it decorative) when no label is given.
|
|
176
|
+
img.setAttribute('alt', this._getLabelText());
|
|
141
177
|
// Reset the output before adding the image
|
|
142
178
|
this.innerHTML = '';
|
|
143
179
|
valueWrapper.appendChild(img);
|