@optionfactory/fml 9.0.0-rc6 → 9.0.0-rc8
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 +1 -1
- package/dist/custom-elements.json +18 -4
- package/dist/fml.css +4 -4
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +188 -24
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ful.css +4 -4
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +20 -1
- package/dist/ful.iife.js +188 -24
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +188 -24
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +9 -1
- package/dist/web-types.json +21 -5
- package/package.json +1 -1
package/dist/fml.iife.js
CHANGED
|
@@ -9088,6 +9088,14 @@ var fml = (function (exports) {
|
|
|
9088
9088
|
if (!this._interactive()) {
|
|
9089
9089
|
return;
|
|
9090
9090
|
}
|
|
9091
|
+
//a click on another control inside the select is that control's, not
|
|
9092
|
+
//the select's: a tooltip marker slotted into `info`, a button a page
|
|
9093
|
+
//put in an affix. Without this, reading the note beside a select
|
|
9094
|
+
//also stole the focus and dropped the dropdown over the note
|
|
9095
|
+
const elsewhere = e.target.closest('button, a[href], input, select, textarea');
|
|
9096
|
+
if (elsewhere && elsewhere !== this.#input) {
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
9091
9099
|
if (this.#ddmenu.shown) {
|
|
9092
9100
|
this.#close();
|
|
9093
9101
|
return;
|
|
@@ -10223,8 +10231,13 @@ var fml = (function (exports) {
|
|
|
10223
10231
|
//started is stale, and neither renders nor updates the request a later
|
|
10224
10232
|
//reload replays, whichever order the responses arrive in
|
|
10225
10233
|
const claim = this.#loads.take();
|
|
10226
|
-
|
|
10227
|
-
|
|
10234
|
+
//the rows stay while the table revalidates. Emptying the body and raising
|
|
10235
|
+
//the spinner row in its place collapsed the table to one tall row and
|
|
10236
|
+
//expanded it again on every sort, page and reload: two layout jumps for
|
|
10237
|
+
//what is the same table with newer rows in it. The spinner is for the load
|
|
10238
|
+
//with nothing to show yet, the first one and the one after a failure; the
|
|
10239
|
+
//rest announce themselves through aria-busy, which the stylesheet reads
|
|
10240
|
+
this.#loading.toggleAttribute('hidden', this.#body.childElementCount > 0);
|
|
10228
10241
|
this.#feedback.setAttribute('hidden', '');
|
|
10229
10242
|
this.#noAutoload.setAttribute('hidden', '');
|
|
10230
10243
|
this.setAttribute('aria-busy', 'true');
|
|
@@ -10242,6 +10255,10 @@ var fml = (function (exports) {
|
|
|
10242
10255
|
return;
|
|
10243
10256
|
}
|
|
10244
10257
|
this.#loading.setAttribute('hidden', '');
|
|
10258
|
+
//the rows the failed load was replacing go with it: what the table
|
|
10259
|
+
//holds is no longer what the request asked for, and leaving them
|
|
10260
|
+
//under the error would say the opposite
|
|
10261
|
+
this.#body.replaceChildren();
|
|
10245
10262
|
this.#feedback.removeAttribute('hidden');
|
|
10246
10263
|
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = Failure.problemsText(
|
|
10247
10264
|
error,
|
|
@@ -10388,6 +10405,20 @@ var fml = (function (exports) {
|
|
|
10388
10405
|
//menu is where the localized words live
|
|
10389
10406
|
this.#button.textContent = this.#display(choice);
|
|
10390
10407
|
Attributes.set(this.#button, 'aria-label', this.#labelFor(choice));
|
|
10408
|
+
this.#mark();
|
|
10409
|
+
}
|
|
10410
|
+
/**
|
|
10411
|
+
* Marks the item the button currently holds, which is what a menu of one
|
|
10412
|
+
* choice among several owes the reader: the glyph on the button says which
|
|
10413
|
+
* one it is only to somebody who already knows the glyphs. The items are
|
|
10414
|
+
* `menuitemradio`, so the state is `aria-checked` rather than the
|
|
10415
|
+
* `aria-selected` a listbox would use, and the stylesheet draws it off that.
|
|
10416
|
+
*/
|
|
10417
|
+
#mark() {
|
|
10418
|
+
const current = this.value;
|
|
10419
|
+
for (const item of this.#items()) {
|
|
10420
|
+
item.setAttribute('aria-checked', String(item.getAttribute('value') === current));
|
|
10421
|
+
}
|
|
10391
10422
|
}
|
|
10392
10423
|
/** The host's disabled claim, composed with the pin: lifting one cannot lift the other. */
|
|
10393
10424
|
set claimed(claimed) {
|
|
@@ -10400,7 +10431,9 @@ var fml = (function (exports) {
|
|
|
10400
10431
|
const li = document.createElement('li');
|
|
10401
10432
|
li.setAttribute('role', 'none');
|
|
10402
10433
|
const a = document.createElement('a');
|
|
10403
|
-
|
|
10434
|
+
//one choice among several, which is what a radio item is: the
|
|
10435
|
+
//state belongs on the item, not on the button alone
|
|
10436
|
+
a.setAttribute('role', 'menuitemradio');
|
|
10404
10437
|
a.setAttribute('tabindex', '-1');
|
|
10405
10438
|
a.setAttribute('value', choice);
|
|
10406
10439
|
const word = this.#labelFor(choice);
|
|
@@ -10418,6 +10451,7 @@ var fml = (function (exports) {
|
|
|
10418
10451
|
return li;
|
|
10419
10452
|
}),
|
|
10420
10453
|
);
|
|
10454
|
+
this.#mark();
|
|
10421
10455
|
}
|
|
10422
10456
|
#sync() {
|
|
10423
10457
|
const pinned = this.pinned;
|
|
@@ -11099,8 +11133,15 @@ var fml = (function (exports) {
|
|
|
11099
11133
|
* `ful-dialog-body` and `ful-dialog-footer` match at any depth, which is what a
|
|
11100
11134
|
* dialog whose content is wrapped in a form needs.
|
|
11101
11135
|
*/
|
|
11136
|
+
/**
|
|
11137
|
+
* How a dialog ended: `dismissed` tells a cancel from an answer, `result` carries
|
|
11138
|
+
* the `data-result` of the button that closed it and `response` what a submit
|
|
11139
|
+
* answered with, the one that did not happen being null.
|
|
11140
|
+
* @typedef {{ dismissed: boolean, result: string|null, response: any }} DialogOutcome
|
|
11141
|
+
*/
|
|
11142
|
+
|
|
11102
11143
|
class Dialog extends ParsedElement {
|
|
11103
|
-
static attributes = ['header', 'requires-answer:presence'];
|
|
11144
|
+
static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence'];
|
|
11104
11145
|
static slots = true;
|
|
11105
11146
|
static template = `
|
|
11106
11147
|
<dialog data-ref="dialog" class="ful-dialog">
|
|
@@ -11108,6 +11149,8 @@ var fml = (function (exports) {
|
|
|
11108
11149
|
<h2 data-tpl-if="header">{{ header }}</h2>
|
|
11109
11150
|
<button data-tpl-if="!requiresAnswer" type="button" data-ref="close" data-tpl-aria-label="#l10n:t('dialog.close')"><ful-icon name="x-lg" aria-hidden="true"></ful-icon></button>
|
|
11110
11151
|
</header>
|
|
11152
|
+
<section data-ref="loading" hidden><ful-spinner class="centered" role="status"><span class="ful-sr-only">{{ #l10n:t('spinner.loading') }}</span></ful-spinner></section>
|
|
11153
|
+
<section data-ref="error" role="alert" hidden></section>
|
|
11111
11154
|
<div data-ref="body" class="ful-dialog-body">{{{{ slots.default }}}}</div>
|
|
11112
11155
|
<footer class="ful-dialog-footer">
|
|
11113
11156
|
<button type="button" data-ref="acknowledge" data-result="acknowledged" data-tpl-if="!slots.buttons" data-tpl-aria-label="#l10n:t('dialog.acknowledge')">{{ #l10n:t('dialog.acknowledge') }}</button>
|
|
@@ -11117,8 +11160,15 @@ var fml = (function (exports) {
|
|
|
11117
11160
|
`;
|
|
11118
11161
|
#dialog;
|
|
11119
11162
|
#body;
|
|
11163
|
+
#loading;
|
|
11164
|
+
#error;
|
|
11120
11165
|
#requests = new SectionRequests();
|
|
11166
|
+
#updates = new Claims();
|
|
11121
11167
|
#resolvers = [];
|
|
11168
|
+
//the answer a submit closed the dialog with, which the return value cannot
|
|
11169
|
+
//carry: it is a string, and a response is whatever the server sent
|
|
11170
|
+
/** @type {DialogOutcome|null} */
|
|
11171
|
+
#answer = null;
|
|
11122
11172
|
render({ slots }) {
|
|
11123
11173
|
const requiresAnswer = this.declared('requires-answer');
|
|
11124
11174
|
const fragment = this.template()
|
|
@@ -11126,13 +11176,12 @@ var fml = (function (exports) {
|
|
|
11126
11176
|
.render();
|
|
11127
11177
|
this.#dialog = fragment.querySelector('[data-ref=dialog]');
|
|
11128
11178
|
this.#body = fragment.querySelector('[data-ref=body]');
|
|
11179
|
+
this.#loading = fragment.querySelector('[data-ref=loading]');
|
|
11180
|
+
this.#error = fragment.querySelector('[data-ref=error]');
|
|
11129
11181
|
this.#dialog.addEventListener('close', () => {
|
|
11130
|
-
this
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
}),
|
|
11134
|
-
);
|
|
11135
|
-
this.#settle();
|
|
11182
|
+
const outcome = this.#outcome();
|
|
11183
|
+
this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
|
|
11184
|
+
this.#settle(outcome);
|
|
11136
11185
|
});
|
|
11137
11186
|
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
11138
11187
|
const result = e.target.closest('button[data-result]')?.dataset.result;
|
|
@@ -11140,11 +11189,26 @@ var fml = (function (exports) {
|
|
|
11140
11189
|
this.#dialog.close(result);
|
|
11141
11190
|
}
|
|
11142
11191
|
});
|
|
11143
|
-
//dismissal, not an answer: the waiters are settled with
|
|
11144
|
-
//Optional because a subclass overriding the template owns
|
|
11192
|
+
//dismissal, not an answer: the waiters are settled with a dismissal, as
|
|
11193
|
+
//Escape does. Optional because a subclass overriding the template owns
|
|
11194
|
+
//what it renders
|
|
11145
11195
|
fragment
|
|
11146
11196
|
.querySelector('[data-ref=close]')
|
|
11147
11197
|
?.addEventListener('click', () => this.#dialog.close(''));
|
|
11198
|
+
if (this.declared('close-on-submit')) {
|
|
11199
|
+
//delegated on the body rather than bound to the form, so a body
|
|
11200
|
+
//delivered later by update() is covered by the same listener. The
|
|
11201
|
+
//form must be the body's own: a ful-table wraps its filters in a
|
|
11202
|
+
//ful-form of its own, and a search in a table the dialog holds is
|
|
11203
|
+
//not the dialog being answered
|
|
11204
|
+
this.#body.addEventListener('submit:success', (/** @type any */ e) => {
|
|
11205
|
+
if (e.target !== Nodes.queryChildren(this.#body, 'ful-form')) {
|
|
11206
|
+
return;
|
|
11207
|
+
}
|
|
11208
|
+
this.#answer = { dismissed: false, result: null, response: e.detail.response };
|
|
11209
|
+
this.#dialog.close('submitted');
|
|
11210
|
+
});
|
|
11211
|
+
}
|
|
11148
11212
|
if (requiresAnswer) {
|
|
11149
11213
|
//the platform's own dismissal, refused where the dialog must be
|
|
11150
11214
|
//answered: cancel fires for Escape and for a close request the
|
|
@@ -11154,39 +11218,96 @@ var fml = (function (exports) {
|
|
|
11154
11218
|
this.replaceChildren(fragment);
|
|
11155
11219
|
wireTargets();
|
|
11156
11220
|
}
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11221
|
+
/**
|
|
11222
|
+
* How the dialog ended, in one shape for every way it can end: `dismissed`
|
|
11223
|
+
* alone tells a cancel from an answer, so a submit answering with no body at
|
|
11224
|
+
* all (a 204) is still an answer, where a bare `null` could not say which it
|
|
11225
|
+
* was. `result` carries the `data-result` of the button that closed it and
|
|
11226
|
+
* `response` what a submit answered with; the one that did not happen is null.
|
|
11227
|
+
*/
|
|
11228
|
+
#outcome() {
|
|
11229
|
+
if (this.#answer) {
|
|
11230
|
+
return this.#answer;
|
|
11231
|
+
}
|
|
11232
|
+
const result = this.#dialog.returnValue;
|
|
11233
|
+
return result === ''
|
|
11234
|
+
? { dismissed: true, result: null, response: null }
|
|
11235
|
+
: { dismissed: false, result, response: null };
|
|
11236
|
+
}
|
|
11237
|
+
//answers every waiter with the dialog's own answer: a dismissal while still
|
|
11238
|
+
//open or closed without a result, which is also the unanswered answer a
|
|
11239
|
+
//dialog leaving the document owes its waiters instead of hanging them
|
|
11240
|
+
#settle(outcome) {
|
|
11161
11241
|
const resolvers = this.#resolvers;
|
|
11162
11242
|
this.#resolvers = [];
|
|
11163
11243
|
for (const resolve of resolvers) {
|
|
11164
|
-
resolve(
|
|
11244
|
+
resolve(outcome);
|
|
11165
11245
|
}
|
|
11166
11246
|
}
|
|
11167
11247
|
disconnectedCallback() {
|
|
11168
|
-
this.#settle();
|
|
11248
|
+
this.#settle(this.#outcome());
|
|
11169
11249
|
}
|
|
11170
11250
|
open() {
|
|
11171
11251
|
return this.ask();
|
|
11172
11252
|
}
|
|
11173
11253
|
ask() {
|
|
11174
|
-
if (
|
|
11175
|
-
this.#
|
|
11176
|
-
this.#dialog.showModal();
|
|
11254
|
+
if (this.#show()) {
|
|
11255
|
+
this.#restChrome();
|
|
11177
11256
|
this.#request();
|
|
11178
11257
|
}
|
|
11179
11258
|
return new Promise((resolve) => {
|
|
11180
11259
|
this.#resolvers.push(resolve);
|
|
11181
11260
|
});
|
|
11182
11261
|
}
|
|
11262
|
+
/**
|
|
11263
|
+
* Opens the dialog and waits for the callback, as `ful-drawer`'s does: a
|
|
11264
|
+
* resolved value paints the body (which is returned), a rejection paints the
|
|
11265
|
+
* problems and travels to the caller, and an update superseded by a newer one
|
|
11266
|
+
* paints nothing. The title is the `header` attribute, configuration like the
|
|
11267
|
+
* rest of the dialog's chrome, so what update() owns is the body alone.
|
|
11268
|
+
*/
|
|
11269
|
+
async update(cb) {
|
|
11270
|
+
//the claim detaches any update still in flight: its outcome belongs to
|
|
11271
|
+
//an abandoned opening and must neither be painted nor own the dialog
|
|
11272
|
+
const claim = this.#updates.take();
|
|
11273
|
+
this.#body.replaceChildren();
|
|
11274
|
+
this.#restChrome();
|
|
11275
|
+
this.#loading?.removeAttribute('hidden');
|
|
11276
|
+
this.#body.setAttribute('hidden', '');
|
|
11277
|
+
//update owns its own open-answer-deliver cycle, so it shows the dialog
|
|
11278
|
+
//without going through ask(): a user reopen during the wait is a real
|
|
11279
|
+
//open and goes through ask()
|
|
11280
|
+
this.#show();
|
|
11281
|
+
try {
|
|
11282
|
+
const delivered = await cb();
|
|
11283
|
+
if (claim.stale) {
|
|
11284
|
+
return this.#body;
|
|
11285
|
+
}
|
|
11286
|
+
this.#body.replaceChildren(delivered);
|
|
11287
|
+
this.#loading?.setAttribute('hidden', '');
|
|
11288
|
+
this.#body.removeAttribute('hidden');
|
|
11289
|
+
return this.#body;
|
|
11290
|
+
} catch (/** @type any */ e) {
|
|
11291
|
+
if (!claim.stale) {
|
|
11292
|
+
//revealed before it is filled, so the live region announces the
|
|
11293
|
+
//change rather than being revealed already holding it
|
|
11294
|
+
this.#error?.removeAttribute('hidden');
|
|
11295
|
+
if (this.#error) {
|
|
11296
|
+
this.#error.textContent = Failure.problemsText(e);
|
|
11297
|
+
}
|
|
11298
|
+
this.#loading?.setAttribute('hidden', '');
|
|
11299
|
+
this.#body.setAttribute('hidden', '');
|
|
11300
|
+
}
|
|
11301
|
+
throw e;
|
|
11302
|
+
}
|
|
11303
|
+
}
|
|
11183
11304
|
#request() {
|
|
11184
11305
|
this.#requests.request(this, this.#body, null, null)?.catch(() => undefined);
|
|
11185
11306
|
}
|
|
11186
11307
|
/**
|
|
11187
11308
|
* Re-fires section:requested on the body, open or closed: the explicit
|
|
11188
11309
|
* request for a body that wants refreshing. A failed refresh paints its
|
|
11189
|
-
* problems, nothing rejects:
|
|
11310
|
+
* problems, nothing rejects: update() stays the rejecting call.
|
|
11190
11311
|
*/
|
|
11191
11312
|
refresh() {
|
|
11192
11313
|
return this.#requests.request(this, this.#body, null, null)?.then(undefined, () => undefined);
|
|
@@ -11194,6 +11315,25 @@ var fml = (function (exports) {
|
|
|
11194
11315
|
close(result) {
|
|
11195
11316
|
this.#dialog.close(result ?? '');
|
|
11196
11317
|
}
|
|
11318
|
+
/** Shows the modal, answering whether this call is the one that opened it. */
|
|
11319
|
+
#show() {
|
|
11320
|
+
if (this.#dialog.open) {
|
|
11321
|
+
return false;
|
|
11322
|
+
}
|
|
11323
|
+
//an opening owes nothing to the one before it: the platform keeps
|
|
11324
|
+
//returnValue across a close with no result, and the answer a submit
|
|
11325
|
+
//left is just as stale
|
|
11326
|
+
this.#dialog.returnValue = '';
|
|
11327
|
+
this.#answer = null;
|
|
11328
|
+
this.#dialog.showModal();
|
|
11329
|
+
return true;
|
|
11330
|
+
}
|
|
11331
|
+
#restChrome() {
|
|
11332
|
+
this.#error?.replaceChildren();
|
|
11333
|
+
this.#error?.setAttribute('hidden', '');
|
|
11334
|
+
this.#loading?.setAttribute('hidden', '');
|
|
11335
|
+
this.#body?.removeAttribute('hidden');
|
|
11336
|
+
}
|
|
11197
11337
|
}
|
|
11198
11338
|
|
|
11199
11339
|
/**
|
|
@@ -11206,7 +11346,7 @@ var fml = (function (exports) {
|
|
|
11206
11346
|
* it.
|
|
11207
11347
|
*/
|
|
11208
11348
|
class Drawer extends ParsedElement {
|
|
11209
|
-
static attributes = ['title', 'placement'];
|
|
11349
|
+
static attributes = ['title', 'placement', 'close-on-submit:presence'];
|
|
11210
11350
|
static slots = true;
|
|
11211
11351
|
static template = `
|
|
11212
11352
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
@@ -11227,6 +11367,10 @@ var fml = (function (exports) {
|
|
|
11227
11367
|
#content;
|
|
11228
11368
|
#requests = new SectionRequests();
|
|
11229
11369
|
#updates = new Claims();
|
|
11370
|
+
//what a submit closed the drawer with, told from a close of any other kind:
|
|
11371
|
+
//a save answering with no body at all is still a save
|
|
11372
|
+
/** @type {{ dismissed: boolean, response: any }|null} */
|
|
11373
|
+
#answer = null;
|
|
11230
11374
|
render({ slots }) {
|
|
11231
11375
|
const fragment = this.template()
|
|
11232
11376
|
.withOverlay({ slots, title: this.declared('title') ?? '' })
|
|
@@ -11242,8 +11386,25 @@ var fml = (function (exports) {
|
|
|
11242
11386
|
}
|
|
11243
11387
|
fragment.querySelector('[data-ref=close]').addEventListener('click', () => this.close());
|
|
11244
11388
|
this.#dialog.addEventListener('close', () => {
|
|
11245
|
-
this.dispatchEvent(
|
|
11389
|
+
this.dispatchEvent(
|
|
11390
|
+
new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),
|
|
11391
|
+
);
|
|
11246
11392
|
});
|
|
11393
|
+
if (this.declared('close-on-submit')) {
|
|
11394
|
+
//delegated on the content section rather than bound to the form: a
|
|
11395
|
+
//drawer's form usually arrives with an update() rather than with the
|
|
11396
|
+
//page, and the section outlives every delivery. The form must be the
|
|
11397
|
+
//content's own, a ful-table wrapping its filters in a ful-form of its
|
|
11398
|
+
//own and a search in a table the drawer holds not being the drawer
|
|
11399
|
+
//finishing
|
|
11400
|
+
this.#content.addEventListener('submit:success', (/** @type any */ e) => {
|
|
11401
|
+
if (e.target !== Nodes.queryChildren(this.#content, 'ful-form')) {
|
|
11402
|
+
return;
|
|
11403
|
+
}
|
|
11404
|
+
this.#answer = { dismissed: false, response: e.detail.response };
|
|
11405
|
+
this.close();
|
|
11406
|
+
});
|
|
11407
|
+
}
|
|
11247
11408
|
this.replaceChildren(fragment);
|
|
11248
11409
|
wireTargets();
|
|
11249
11410
|
}
|
|
@@ -11316,6 +11477,9 @@ var fml = (function (exports) {
|
|
|
11316
11477
|
if (this.#dialog.open) {
|
|
11317
11478
|
return false;
|
|
11318
11479
|
}
|
|
11480
|
+
//an opening owes nothing to the one before it: the answer a submit left
|
|
11481
|
+
//belongs to the drawer that closed on it
|
|
11482
|
+
this.#answer = null;
|
|
11319
11483
|
this.#dialog.showModal();
|
|
11320
11484
|
return true;
|
|
11321
11485
|
}
|