@optionfactory/fml 9.0.0-rc7 → 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 +1 -1
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +151 -21
- 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 +1 -1
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +20 -1
- package/dist/ful.iife.js +151 -21
- 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 +151 -21
- 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
|
@@ -11133,8 +11133,15 @@ var fml = (function (exports) {
|
|
|
11133
11133
|
* `ful-dialog-body` and `ful-dialog-footer` match at any depth, which is what a
|
|
11134
11134
|
* dialog whose content is wrapped in a form needs.
|
|
11135
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
|
+
|
|
11136
11143
|
class Dialog extends ParsedElement {
|
|
11137
|
-
static attributes = ['header', 'requires-answer:presence'];
|
|
11144
|
+
static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence'];
|
|
11138
11145
|
static slots = true;
|
|
11139
11146
|
static template = `
|
|
11140
11147
|
<dialog data-ref="dialog" class="ful-dialog">
|
|
@@ -11142,6 +11149,8 @@ var fml = (function (exports) {
|
|
|
11142
11149
|
<h2 data-tpl-if="header">{{ header }}</h2>
|
|
11143
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>
|
|
11144
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>
|
|
11145
11154
|
<div data-ref="body" class="ful-dialog-body">{{{{ slots.default }}}}</div>
|
|
11146
11155
|
<footer class="ful-dialog-footer">
|
|
11147
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>
|
|
@@ -11151,8 +11160,15 @@ var fml = (function (exports) {
|
|
|
11151
11160
|
`;
|
|
11152
11161
|
#dialog;
|
|
11153
11162
|
#body;
|
|
11163
|
+
#loading;
|
|
11164
|
+
#error;
|
|
11154
11165
|
#requests = new SectionRequests();
|
|
11166
|
+
#updates = new Claims();
|
|
11155
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;
|
|
11156
11172
|
render({ slots }) {
|
|
11157
11173
|
const requiresAnswer = this.declared('requires-answer');
|
|
11158
11174
|
const fragment = this.template()
|
|
@@ -11160,13 +11176,12 @@ var fml = (function (exports) {
|
|
|
11160
11176
|
.render();
|
|
11161
11177
|
this.#dialog = fragment.querySelector('[data-ref=dialog]');
|
|
11162
11178
|
this.#body = fragment.querySelector('[data-ref=body]');
|
|
11179
|
+
this.#loading = fragment.querySelector('[data-ref=loading]');
|
|
11180
|
+
this.#error = fragment.querySelector('[data-ref=error]');
|
|
11163
11181
|
this.#dialog.addEventListener('close', () => {
|
|
11164
|
-
this
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
}),
|
|
11168
|
-
);
|
|
11169
|
-
this.#settle();
|
|
11182
|
+
const outcome = this.#outcome();
|
|
11183
|
+
this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
|
|
11184
|
+
this.#settle(outcome);
|
|
11170
11185
|
});
|
|
11171
11186
|
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
11172
11187
|
const result = e.target.closest('button[data-result]')?.dataset.result;
|
|
@@ -11174,11 +11189,26 @@ var fml = (function (exports) {
|
|
|
11174
11189
|
this.#dialog.close(result);
|
|
11175
11190
|
}
|
|
11176
11191
|
});
|
|
11177
|
-
//dismissal, not an answer: the waiters are settled with
|
|
11178
|
-
//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
|
|
11179
11195
|
fragment
|
|
11180
11196
|
.querySelector('[data-ref=close]')
|
|
11181
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
|
+
}
|
|
11182
11212
|
if (requiresAnswer) {
|
|
11183
11213
|
//the platform's own dismissal, refused where the dialog must be
|
|
11184
11214
|
//answered: cancel fires for Escape and for a close request the
|
|
@@ -11188,39 +11218,96 @@ var fml = (function (exports) {
|
|
|
11188
11218
|
this.replaceChildren(fragment);
|
|
11189
11219
|
wireTargets();
|
|
11190
11220
|
}
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
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) {
|
|
11195
11241
|
const resolvers = this.#resolvers;
|
|
11196
11242
|
this.#resolvers = [];
|
|
11197
11243
|
for (const resolve of resolvers) {
|
|
11198
|
-
resolve(
|
|
11244
|
+
resolve(outcome);
|
|
11199
11245
|
}
|
|
11200
11246
|
}
|
|
11201
11247
|
disconnectedCallback() {
|
|
11202
|
-
this.#settle();
|
|
11248
|
+
this.#settle(this.#outcome());
|
|
11203
11249
|
}
|
|
11204
11250
|
open() {
|
|
11205
11251
|
return this.ask();
|
|
11206
11252
|
}
|
|
11207
11253
|
ask() {
|
|
11208
|
-
if (
|
|
11209
|
-
this.#
|
|
11210
|
-
this.#dialog.showModal();
|
|
11254
|
+
if (this.#show()) {
|
|
11255
|
+
this.#restChrome();
|
|
11211
11256
|
this.#request();
|
|
11212
11257
|
}
|
|
11213
11258
|
return new Promise((resolve) => {
|
|
11214
11259
|
this.#resolvers.push(resolve);
|
|
11215
11260
|
});
|
|
11216
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
|
+
}
|
|
11217
11304
|
#request() {
|
|
11218
11305
|
this.#requests.request(this, this.#body, null, null)?.catch(() => undefined);
|
|
11219
11306
|
}
|
|
11220
11307
|
/**
|
|
11221
11308
|
* Re-fires section:requested on the body, open or closed: the explicit
|
|
11222
11309
|
* request for a body that wants refreshing. A failed refresh paints its
|
|
11223
|
-
* problems, nothing rejects:
|
|
11310
|
+
* problems, nothing rejects: update() stays the rejecting call.
|
|
11224
11311
|
*/
|
|
11225
11312
|
refresh() {
|
|
11226
11313
|
return this.#requests.request(this, this.#body, null, null)?.then(undefined, () => undefined);
|
|
@@ -11228,6 +11315,25 @@ var fml = (function (exports) {
|
|
|
11228
11315
|
close(result) {
|
|
11229
11316
|
this.#dialog.close(result ?? '');
|
|
11230
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
|
+
}
|
|
11231
11337
|
}
|
|
11232
11338
|
|
|
11233
11339
|
/**
|
|
@@ -11240,7 +11346,7 @@ var fml = (function (exports) {
|
|
|
11240
11346
|
* it.
|
|
11241
11347
|
*/
|
|
11242
11348
|
class Drawer extends ParsedElement {
|
|
11243
|
-
static attributes = ['title', 'placement'];
|
|
11349
|
+
static attributes = ['title', 'placement', 'close-on-submit:presence'];
|
|
11244
11350
|
static slots = true;
|
|
11245
11351
|
static template = `
|
|
11246
11352
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
@@ -11261,6 +11367,10 @@ var fml = (function (exports) {
|
|
|
11261
11367
|
#content;
|
|
11262
11368
|
#requests = new SectionRequests();
|
|
11263
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;
|
|
11264
11374
|
render({ slots }) {
|
|
11265
11375
|
const fragment = this.template()
|
|
11266
11376
|
.withOverlay({ slots, title: this.declared('title') ?? '' })
|
|
@@ -11276,8 +11386,25 @@ var fml = (function (exports) {
|
|
|
11276
11386
|
}
|
|
11277
11387
|
fragment.querySelector('[data-ref=close]').addEventListener('click', () => this.close());
|
|
11278
11388
|
this.#dialog.addEventListener('close', () => {
|
|
11279
|
-
this.dispatchEvent(
|
|
11389
|
+
this.dispatchEvent(
|
|
11390
|
+
new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),
|
|
11391
|
+
);
|
|
11280
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
|
+
}
|
|
11281
11408
|
this.replaceChildren(fragment);
|
|
11282
11409
|
wireTargets();
|
|
11283
11410
|
}
|
|
@@ -11350,6 +11477,9 @@ var fml = (function (exports) {
|
|
|
11350
11477
|
if (this.#dialog.open) {
|
|
11351
11478
|
return false;
|
|
11352
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;
|
|
11353
11483
|
this.#dialog.showModal();
|
|
11354
11484
|
return true;
|
|
11355
11485
|
}
|