@optionfactory/fml 9.0.0-rc30 → 9.0.0-rc31
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/dist/custom-elements.json +8 -1
- package/dist/fml.css +6 -6
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +234 -77
- 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/ftl.d.mts +32 -9
- package/dist/ftl.iife.js +89 -25
- package/dist/ftl.iife.js.map +1 -1
- package/dist/ftl.iife.min.js +1 -1
- package/dist/ftl.iife.min.js.map +1 -1
- package/dist/ftl.min.mjs +1 -1
- package/dist/ftl.min.mjs.map +1 -1
- package/dist/ftl.mjs +89 -25
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +6 -6
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +57 -62
- package/dist/ful.iife.js +145 -51
- 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 +147 -52
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +5 -1
- package/dist/web-types.json +10 -2
- package/package.json +2 -2
package/dist/ful.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParsedElement, Attributes, Rendering, Localization, BoundedCache,
|
|
1
|
+
import { ParsedElement, Attributes, Fragments, Rendering, Localization, BoundedCache, Templates, Nodes } from './ftl.mjs';
|
|
2
2
|
import { Failure, HttpClient } from './httpc.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -798,12 +798,13 @@ class Bindings {
|
|
|
798
798
|
*
|
|
799
799
|
* The claim setters, the validity protocol and the aria wiring all act on the
|
|
800
800
|
* pieces, so a field with no native control returns a focusable piece of its
|
|
801
|
-
* own chrome as the control. The claims, `value
|
|
802
|
-
* members that tolerate a not yet rendered
|
|
803
|
-
* `value` getter answers what the rendered
|
|
804
|
-
* declared state,
|
|
805
|
-
* (see `_holdValue`)
|
|
806
|
-
* the
|
|
801
|
+
* own chrome as the control. The claims, `value`, `focus()` and
|
|
802
|
+
* `setCustomValidity` are the members that tolerate a not yet rendered
|
|
803
|
+
* element: before the render the `value` getter answers what the rendered
|
|
804
|
+
* field would answer from its current declared state, a value written then is
|
|
805
|
+
* held and applied by the render (see `_holdValue`), and a problem reported
|
|
806
|
+
* then is shown by it. The other members need the pieces and so work only
|
|
807
|
+
* after the render.
|
|
807
808
|
*
|
|
808
809
|
* The inner controls carry `form=""`, so the host is the only element the form
|
|
809
810
|
* sees. Enter pressed in an inner `<input>` of any type but `file`, `button`,
|
|
@@ -841,6 +842,8 @@ class Field extends ParsedElement {
|
|
|
841
842
|
#also = [];
|
|
842
843
|
/** @type {{ value: any } | null} the value written before the render, which the render applies */
|
|
843
844
|
#held = null;
|
|
845
|
+
/** @type {{ error: string, context?: string } | null} the problem reported before the render, which the render shows */
|
|
846
|
+
#reported = null;
|
|
844
847
|
/** Sets the internals' role from the subclass's `static ROLE`. */
|
|
845
848
|
constructor() {
|
|
846
849
|
super();
|
|
@@ -905,6 +908,11 @@ class Field extends ParsedElement {
|
|
|
905
908
|
this.#pendingFocus = null;
|
|
906
909
|
control.focus(options);
|
|
907
910
|
}
|
|
911
|
+
if (this.#reported !== null) {
|
|
912
|
+
const { error, context } = this.#reported;
|
|
913
|
+
this.#reported = null;
|
|
914
|
+
this.setCustomValidity(error, context);
|
|
915
|
+
}
|
|
908
916
|
}
|
|
909
917
|
/**
|
|
910
918
|
* @param {EventTarget} el
|
|
@@ -984,11 +992,17 @@ class Field extends ParsedElement {
|
|
|
984
992
|
* field owning a whole subtree overrides this to route the message to the
|
|
985
993
|
* inner control the path names.
|
|
986
994
|
*
|
|
987
|
-
*
|
|
995
|
+
* Before the render there is no error region to write to: a message is
|
|
996
|
+
* kept (the last one wins) and shown once the field has rendered, and a
|
|
997
|
+
* clear drops the one kept. A field whose render failed shows nothing.
|
|
988
998
|
* @param {string} [error] the message, empty or missing to clear
|
|
989
999
|
* @param {string} [context] the path below this field's name, '' on an exact match
|
|
990
1000
|
*/
|
|
991
1001
|
setCustomValidity(error, context) {
|
|
1002
|
+
if (!this.#fieldError) {
|
|
1003
|
+
this.#reported = error ? { error, context } : null;
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
992
1006
|
Attributes.set(this.#announces ?? this.#control, 'aria-invalid', error ? 'true' : null);
|
|
993
1007
|
if (!error) {
|
|
994
1008
|
this.internals.setValidity({});
|
|
@@ -1199,11 +1213,16 @@ class Field extends ParsedElement {
|
|
|
1199
1213
|
/**
|
|
1200
1214
|
* Calls `_build(conf)` and wires the pieces it answers. When `_build`
|
|
1201
1215
|
* returns a promise the pieces are wired once it settles; a field that
|
|
1202
|
-
* builds synchronously renders synchronously.
|
|
1216
|
+
* builds synchronously renders synchronously. A default slot holding
|
|
1217
|
+
* nothing but whitespace is emptied first, so the label it becomes is
|
|
1218
|
+
* `:empty` and takes no row.
|
|
1203
1219
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
1204
1220
|
* @returns {void | Promise<void>}
|
|
1205
1221
|
*/
|
|
1206
1222
|
render(conf) {
|
|
1223
|
+
if (conf.slots?.default && Fragments.isBlank(conf.slots.default)) {
|
|
1224
|
+
conf.slots.default.replaceChildren();
|
|
1225
|
+
}
|
|
1207
1226
|
const built = /** @type {any} */ (this._build(conf));
|
|
1208
1227
|
if (built instanceof Promise) {
|
|
1209
1228
|
return built.then((pieces) => this.#wire(pieces));
|
|
@@ -2543,7 +2562,8 @@ class InputInstant extends Input {
|
|
|
2543
2562
|
* size and count limits it declares.
|
|
2544
2563
|
*
|
|
2545
2564
|
* The `dropzone` and `item-list` attributes show the dropzone and the list of
|
|
2546
|
-
* chosen files
|
|
2565
|
+
* chosen files, and `no-input` takes the picker control out of sight, leaving
|
|
2566
|
+
* it to the keyboard and to assistive technology. A `dropzone` slot replaces the default dropzone's content, and
|
|
2547
2567
|
* an `items` slot holding a template replaces the stock item, rendered with the
|
|
2548
2568
|
* same `files` overlay so that it reads the File objects it lists. Clicking the
|
|
2549
2569
|
* dropzone opens the picker, and the host carries a `dragover` attribute while
|
|
@@ -2586,6 +2606,7 @@ class InputFile extends Input {
|
|
|
2586
2606
|
'multiple:presence',
|
|
2587
2607
|
'item-list:presence',
|
|
2588
2608
|
'dropzone:presence',
|
|
2609
|
+
'no-input:presence',
|
|
2589
2610
|
'max-files:number',
|
|
2590
2611
|
'max-file-size:number',
|
|
2591
2612
|
'max-total-size:number',
|
|
@@ -2837,7 +2858,7 @@ class InputFile extends Input {
|
|
|
2837
2858
|
* @returns {string[]|string|null}
|
|
2838
2859
|
*/
|
|
2839
2860
|
get value() {
|
|
2840
|
-
const names = Array.from(this.
|
|
2861
|
+
const names = Array.from(this.files).map((f) => f.name);
|
|
2841
2862
|
return this.multiple ? names : (names[0] ?? null);
|
|
2842
2863
|
}
|
|
2843
2864
|
/**
|
|
@@ -2860,7 +2881,7 @@ class InputFile extends Input {
|
|
|
2860
2881
|
* The size of the selected files together, in bytes.
|
|
2861
2882
|
* @returns {number}
|
|
2862
2883
|
*/
|
|
2863
|
-
get
|
|
2884
|
+
get totalSize() {
|
|
2864
2885
|
return Array.from(this.files).reduce((a, f) => a + f.size, 0);
|
|
2865
2886
|
}
|
|
2866
2887
|
#maxFiles;
|
|
@@ -2935,6 +2956,23 @@ class InputFile extends Input {
|
|
|
2935
2956
|
this.#useDropzone = v;
|
|
2936
2957
|
this.reflectTo('dropzone', v);
|
|
2937
2958
|
}
|
|
2959
|
+
#noInput;
|
|
2960
|
+
/**
|
|
2961
|
+
* Whether the picker control is taken out of sight, the dropzone and the
|
|
2962
|
+
* item list carrying the field alone; the stylesheet hides it from the
|
|
2963
|
+
* reflected `no-input` attribute. The control stays in the tab order and
|
|
2964
|
+
* keeps the label, the description and the error, and the dropzone shows
|
|
2965
|
+
* the focus ring while it holds the focus.
|
|
2966
|
+
* @returns {boolean}
|
|
2967
|
+
*/
|
|
2968
|
+
get noInput() {
|
|
2969
|
+
return this.#noInput;
|
|
2970
|
+
}
|
|
2971
|
+
/** @param {boolean} v */
|
|
2972
|
+
set noInput(v) {
|
|
2973
|
+
this.#noInput = v;
|
|
2974
|
+
this.reflectTo('no-input', v);
|
|
2975
|
+
}
|
|
2938
2976
|
}
|
|
2939
2977
|
|
|
2940
2978
|
/** the viewport's breathing room when clamping, in pixels */
|
|
@@ -4698,9 +4736,7 @@ class RadioGroup extends Field {
|
|
|
4698
4736
|
static ROLE = 'radiogroup';
|
|
4699
4737
|
static template = `
|
|
4700
4738
|
<fieldset>
|
|
4701
|
-
<legend>
|
|
4702
|
-
{{{{ slots.default }}}}
|
|
4703
|
-
</legend>
|
|
4739
|
+
<legend>{{{{ slots.default }}}}</legend>
|
|
4704
4740
|
<header data-tpl-if="slots.header">
|
|
4705
4741
|
{{{{ slots.header }}}}
|
|
4706
4742
|
</header>
|
|
@@ -4748,6 +4784,9 @@ class RadioGroup extends Field {
|
|
|
4748
4784
|
radioEls.forEach((el) => {
|
|
4749
4785
|
el.remove();
|
|
4750
4786
|
});
|
|
4787
|
+
if (Fragments.isBlank(slots.default)) {
|
|
4788
|
+
slots.default.replaceChildren();
|
|
4789
|
+
}
|
|
4751
4790
|
const fragment = this.template().withOverlay({ name, slots, inputsAndLabels }).render();
|
|
4752
4791
|
this.#fieldset = /** @type HTMLElement */ (fragment.firstElementChild);
|
|
4753
4792
|
this.#firstRadio = fragment.querySelector('input[type=radio]');
|
|
@@ -4903,6 +4942,10 @@ class Checkbox extends Field {
|
|
|
4903
4942
|
* current `order` in the cycle `asc`, `desc`, `null`. The button does not
|
|
4904
4943
|
* change its own `order`: whoever handles the event sets it, which `ful-table`
|
|
4905
4944
|
* does once the sorted page has loaded.
|
|
4945
|
+
*
|
|
4946
|
+
* The stylesheet draws an up arrow for `asc` and a down arrow for `desc`. An
|
|
4947
|
+
* unsorted button shows the up arrow dimmed, `asc` being the order its first
|
|
4948
|
+
* activation asks for.
|
|
4906
4949
|
*/
|
|
4907
4950
|
class SortButton extends ParsedElement {
|
|
4908
4951
|
static attributes = ['sorter'];
|
|
@@ -5479,18 +5522,19 @@ class Table extends ParsedElement {
|
|
|
5479
5522
|
* writing the size is not a request to start loading, which is what
|
|
5480
5523
|
* `autoload` and `reload()` are for. A write that does not change the size
|
|
5481
5524
|
* does nothing. A write made before the render is replaced by the declared
|
|
5482
|
-
* `page-size` when the table renders
|
|
5525
|
+
* `page-size` when the table renders, or by ten when that is not a number
|
|
5526
|
+
* above zero.
|
|
5483
5527
|
*
|
|
5484
5528
|
* The reload is not awaited: a failure shows the error panel and its
|
|
5485
5529
|
* rejection is unhandled.
|
|
5486
5530
|
* @param {number|string|null|undefined} value a number or a string of
|
|
5487
5531
|
* one, such as a select's value; absent or null is the default of ten,
|
|
5488
5532
|
* so removing the attribute restores it, and a value that is not a
|
|
5489
|
-
* number is ignored
|
|
5533
|
+
* finite number above zero is ignored
|
|
5490
5534
|
*/
|
|
5491
5535
|
set pageSize(value) {
|
|
5492
5536
|
const size = Number(value ?? 10);
|
|
5493
|
-
if (Number.
|
|
5537
|
+
if (!Number.isFinite(size) || size <= 0 || size === this.#latestRequest.pageRequest.size) {
|
|
5494
5538
|
return;
|
|
5495
5539
|
}
|
|
5496
5540
|
this.#latestRequest = { ...this.#latestRequest, pageRequest: { page: 0, size } };
|
|
@@ -5609,10 +5653,11 @@ class Table extends ParsedElement {
|
|
|
5609
5653
|
await Rendering.waitForChildren(this);
|
|
5610
5654
|
|
|
5611
5655
|
const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
|
|
5656
|
+
const declaredSize = this.declared('page-size');
|
|
5612
5657
|
this.#latestRequest = {
|
|
5613
5658
|
pageRequest: {
|
|
5614
5659
|
page: 0,
|
|
5615
|
-
size:
|
|
5660
|
+
size: Number.isFinite(declaredSize) && declaredSize > 0 ? declaredSize : 10,
|
|
5616
5661
|
},
|
|
5617
5662
|
sortRequest: schema.sort,
|
|
5618
5663
|
filterRequest: maybeForm?.values ?? {},
|
|
@@ -5696,7 +5741,10 @@ class Table extends ParsedElement {
|
|
|
5696
5741
|
* `aria-busy` is removed, `load:failure` is dispatched (bubbling, not
|
|
5697
5742
|
* cancelable) with `detail` set to
|
|
5698
5743
|
* `{ pageRequest, sortRequest, filterRequest, exception }`, and the
|
|
5699
|
-
* returned promise rejects with the same error.
|
|
5744
|
+
* returned promise rejects with the same error. An answer the table cannot
|
|
5745
|
+
* page fails the same way, with an error saying so: one whose `size` is
|
|
5746
|
+
* not a number, as a response of another shape read without a
|
|
5747
|
+
* `response-mapper` is, or a page size of zero.
|
|
5700
5748
|
*
|
|
5701
5749
|
* A load started after this one supersedes it: when this one's answer or
|
|
5702
5750
|
* failure arrives it changes nothing, dispatches nothing and resolves;
|
|
@@ -5724,6 +5772,11 @@ class Table extends ParsedElement {
|
|
|
5724
5772
|
return;
|
|
5725
5773
|
}
|
|
5726
5774
|
lastPage = Math.max(0, Math.ceil(pageResponse.size / pageRequest.size) - 1);
|
|
5775
|
+
if (!Number.isFinite(lastPage)) {
|
|
5776
|
+
throw new Error(
|
|
5777
|
+
`ful-table cannot page the loader's answer: it needs { data, size }, size being the total number of rows, and got a size of ${pageResponse.size} for pages of ${pageRequest.size}. A response of another shape needs a response-mapper`,
|
|
5778
|
+
);
|
|
5779
|
+
}
|
|
5727
5780
|
if (pageRequest.page <= lastPage) {
|
|
5728
5781
|
this.#latestRequest = { pageRequest, sortRequest, filterRequest };
|
|
5729
5782
|
this.#loaded = true;
|
|
@@ -6989,8 +7042,10 @@ class Tooltip extends ParsedElement {
|
|
|
6989
7042
|
* failure's problems, one per line, or its message when it carries none.
|
|
6990
7043
|
*/
|
|
6991
7044
|
class SectionRequests {
|
|
6992
|
-
/** @type {WeakSet<Element>} */
|
|
7045
|
+
/** @type {WeakSet<Element>} the sections a first delivery succeeded for */
|
|
6993
7046
|
#entered = new WeakSet();
|
|
7047
|
+
/** @type {WeakSet<Element>} the sections whose first delivery is in flight */
|
|
7048
|
+
#entering = new WeakSet();
|
|
6994
7049
|
/** @type {WeakMap<Element, Claims>} */
|
|
6995
7050
|
#claims = new WeakMap();
|
|
6996
7051
|
|
|
@@ -6999,9 +7054,12 @@ class SectionRequests {
|
|
|
6999
7054
|
* is given, then `section:requested:<name>` when a name is given, each
|
|
7000
7055
|
* bubbling from `host` with the same detail `{ name, section, index, first }`,
|
|
7001
7056
|
* and awaits every answer attached to any of them. `first` is true until a
|
|
7002
|
-
*
|
|
7003
|
-
*
|
|
7004
|
-
*
|
|
7057
|
+
* delivery of the section succeeds: a request nobody listened to does not
|
|
7058
|
+
* spend it, and neither does one whose answers failed, so the next request
|
|
7059
|
+
* is a first one again. While a first delivery is in flight a newer request
|
|
7060
|
+
* for the same section carries `first` false, so the content is not
|
|
7061
|
+
* delivered twice. A request with answers removes the error a previous
|
|
7062
|
+
* failure painted before it waits.
|
|
7005
7063
|
* @param {Element} host the component the events are dispatched on
|
|
7006
7064
|
* @param {Element} section the element the answers deliver into
|
|
7007
7065
|
* @param {string|null} name the section's `data-step`, when it has one
|
|
@@ -7012,7 +7070,7 @@ class SectionRequests {
|
|
|
7012
7070
|
* problems in the section unless a newer request superseded this one
|
|
7013
7071
|
*/
|
|
7014
7072
|
async request(host, section, name, index) {
|
|
7015
|
-
const first = !this.#entered.has(section);
|
|
7073
|
+
const first = !this.#entered.has(section) && !this.#entering.has(section);
|
|
7016
7074
|
const detail = { name, section, index, first };
|
|
7017
7075
|
const types = [
|
|
7018
7076
|
'section:requested',
|
|
@@ -7030,7 +7088,9 @@ class SectionRequests {
|
|
|
7030
7088
|
if (promises.length === 0) {
|
|
7031
7089
|
return undefined;
|
|
7032
7090
|
}
|
|
7033
|
-
|
|
7091
|
+
if (first) {
|
|
7092
|
+
this.#entering.add(section);
|
|
7093
|
+
}
|
|
7034
7094
|
let claims = this.#claims.get(section);
|
|
7035
7095
|
if (!claims) {
|
|
7036
7096
|
claims = new Claims();
|
|
@@ -7046,13 +7106,20 @@ class SectionRequests {
|
|
|
7046
7106
|
}
|
|
7047
7107
|
});
|
|
7048
7108
|
try {
|
|
7049
|
-
|
|
7109
|
+
const answers = await Promise.all(promises);
|
|
7110
|
+
if (first) {
|
|
7111
|
+
this.#entered.add(section);
|
|
7112
|
+
}
|
|
7113
|
+
return answers;
|
|
7050
7114
|
} catch (cause) {
|
|
7051
7115
|
if (owned()) {
|
|
7052
7116
|
this.#paintError(section, cause);
|
|
7053
7117
|
}
|
|
7054
7118
|
throw cause;
|
|
7055
7119
|
} finally {
|
|
7120
|
+
if (first) {
|
|
7121
|
+
this.#entering.delete(section);
|
|
7122
|
+
}
|
|
7056
7123
|
cancelAnimationFrame(frame);
|
|
7057
7124
|
if (owned()) {
|
|
7058
7125
|
section.toggleAttribute('loading', false);
|
|
@@ -7169,15 +7236,27 @@ class DialogSections {
|
|
|
7169
7236
|
/**
|
|
7170
7237
|
* Calls `dismiss` when a press and its release both land on the backdrop,
|
|
7171
7238
|
* so a selection dragged out of the panel and released outside does not.
|
|
7239
|
+
* The backdrop is where the pointer is outside the dialog's rectangle: a
|
|
7240
|
+
* part of the panel no child covers, the room around the error section
|
|
7241
|
+
* for one, is the panel.
|
|
7172
7242
|
* @param {() => void} dismiss
|
|
7173
7243
|
*/
|
|
7174
7244
|
onBackdrop(dismiss) {
|
|
7245
|
+
const onBackdrop = (/** @type MouseEvent */ e) => {
|
|
7246
|
+
if (e.target !== this.#dialog) {
|
|
7247
|
+
return false;
|
|
7248
|
+
}
|
|
7249
|
+
const panel = this.#dialog.getBoundingClientRect();
|
|
7250
|
+
return (
|
|
7251
|
+
e.clientX < panel.left || e.clientX > panel.right || e.clientY < panel.top || e.clientY > panel.bottom
|
|
7252
|
+
);
|
|
7253
|
+
};
|
|
7175
7254
|
let pressedOutside = false;
|
|
7176
7255
|
this.#dialog.addEventListener('mousedown', (e) => {
|
|
7177
|
-
pressedOutside = e
|
|
7256
|
+
pressedOutside = onBackdrop(e);
|
|
7178
7257
|
});
|
|
7179
7258
|
this.#dialog.addEventListener('click', (e) => {
|
|
7180
|
-
if (pressedOutside && e
|
|
7259
|
+
if (pressedOutside && onBackdrop(e)) {
|
|
7181
7260
|
dismiss();
|
|
7182
7261
|
}
|
|
7183
7262
|
});
|
|
@@ -7454,7 +7533,7 @@ class Dialog extends ParsedElement {
|
|
|
7454
7533
|
* opening, hides the error and loading sections, and dispatches
|
|
7455
7534
|
* `section:requested` on the element (bubbling, with
|
|
7456
7535
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
7457
|
-
* the body and `first` true
|
|
7536
|
+
* the body and `first` true until a delivery succeeds), so a
|
|
7458
7537
|
* listener can fill the body. A failed request is painted into the body
|
|
7459
7538
|
* and does not reject. On a dialog already open nothing is shown or
|
|
7460
7539
|
* requested, and the call waits for the same ending as the calls before it;
|
|
@@ -7706,11 +7785,7 @@ class Drawer extends ParsedElement {
|
|
|
7706
7785
|
#answer = null;
|
|
7707
7786
|
#resolvers = [];
|
|
7708
7787
|
#closing = false;
|
|
7709
|
-
#
|
|
7710
|
-
if (e.target === this.#dialog) {
|
|
7711
|
-
this.#closeNow();
|
|
7712
|
-
}
|
|
7713
|
-
};
|
|
7788
|
+
#slides = new Claims();
|
|
7714
7789
|
/**
|
|
7715
7790
|
* @param {{ slots: Record<string, DocumentFragment> }} c
|
|
7716
7791
|
*/
|
|
@@ -7837,7 +7912,7 @@ class Drawer extends ParsedElement {
|
|
|
7837
7912
|
* opening, hides the error and loading sections, and dispatches
|
|
7838
7913
|
* `section:requested` on the element (bubbling, with
|
|
7839
7914
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
7840
|
-
* the content section and `first` true
|
|
7915
|
+
* the content section and `first` true until a delivery succeeds),
|
|
7841
7916
|
* so a listener can fill the content. A failed request is painted into the
|
|
7842
7917
|
* content section and does not reject. On a drawer already open nothing is
|
|
7843
7918
|
* shown or requested, except that one sliding out stops and stays open,
|
|
@@ -7888,11 +7963,13 @@ class Drawer extends ParsedElement {
|
|
|
7888
7963
|
/**
|
|
7889
7964
|
* Slides the drawer out and closes it when the animation ends, setting the
|
|
7890
7965
|
* `closing` attribute on the native dialog meanwhile; the `close` event is
|
|
7891
|
-
* dispatched then, after this call has returned.
|
|
7892
|
-
*
|
|
7893
|
-
*
|
|
7894
|
-
*
|
|
7895
|
-
*
|
|
7966
|
+
* dispatched then, after this call has returned. An animation the page
|
|
7967
|
+
* cancels, by hiding the drawer or an ancestor of it, closes it as one
|
|
7968
|
+
* that ended does. Where the user prefers reduced motion, or no animation
|
|
7969
|
+
* runs, it closes at once. A non-empty result answers as the button
|
|
7970
|
+
* carrying that `data-result` would; no result, or an empty one, is a
|
|
7971
|
+
* dismissal. Does nothing on a drawer that is closed or already sliding
|
|
7972
|
+
* out.
|
|
7896
7973
|
* @param {string} [result]
|
|
7897
7974
|
*/
|
|
7898
7975
|
close(result) {
|
|
@@ -7907,12 +7984,18 @@ class Drawer extends ParsedElement {
|
|
|
7907
7984
|
return;
|
|
7908
7985
|
}
|
|
7909
7986
|
this.#dialog.setAttribute('closing', '');
|
|
7910
|
-
|
|
7987
|
+
const animations = this.#dialog.getAnimations();
|
|
7988
|
+
if (animations.length === 0) {
|
|
7911
7989
|
this.#closeNow();
|
|
7912
7990
|
return;
|
|
7913
7991
|
}
|
|
7914
7992
|
this.#closing = true;
|
|
7915
|
-
this.#
|
|
7993
|
+
const slide = this.#slides.take();
|
|
7994
|
+
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
|
7995
|
+
if (!slide.stale) {
|
|
7996
|
+
this.#closeNow();
|
|
7997
|
+
}
|
|
7998
|
+
});
|
|
7916
7999
|
}
|
|
7917
8000
|
#closeNow() {
|
|
7918
8001
|
this.#stopSlidingOut();
|
|
@@ -7920,7 +8003,7 @@ class Drawer extends ParsedElement {
|
|
|
7920
8003
|
}
|
|
7921
8004
|
#stopSlidingOut() {
|
|
7922
8005
|
this.#closing = false;
|
|
7923
|
-
this.#
|
|
8006
|
+
this.#slides.invalidate();
|
|
7924
8007
|
this.#dialog.removeAttribute('closing');
|
|
7925
8008
|
}
|
|
7926
8009
|
/** @returns {boolean} whether this call is the one that opened the modal */
|
|
@@ -8465,7 +8548,7 @@ class Tabs extends ParsedElement {
|
|
|
8465
8548
|
}
|
|
8466
8549
|
/**
|
|
8467
8550
|
* Fires the section requests again for a panel, whether it is visible or
|
|
8468
|
-
* not, with `first` false once an earlier
|
|
8551
|
+
* not, with `first` false once an earlier delivery succeeded.
|
|
8469
8552
|
* @param {number | Element} ref the panel's index, or the panel itself
|
|
8470
8553
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
8471
8554
|
* the answers, or to undefined when nobody listened or the delivery
|
|
@@ -8688,7 +8771,7 @@ class Wizard extends ParsedElement {
|
|
|
8688
8771
|
}
|
|
8689
8772
|
/**
|
|
8690
8773
|
* Fires the section requests again for a section, whether it is current or
|
|
8691
|
-
* not, with `first` false once an earlier
|
|
8774
|
+
* not, with `first` false once an earlier delivery succeeded.
|
|
8692
8775
|
* @param {string | Element} ref the section's `data-step`, or the section
|
|
8693
8776
|
* itself
|
|
8694
8777
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
@@ -8967,8 +9050,11 @@ class Plugin {
|
|
|
8967
9050
|
/**
|
|
8968
9051
|
* @param {{ language?: string, translations?: Record<string, any>, httpClient?: HttpClient }} [options]
|
|
8969
9052
|
* `language` is fixed for the page: a full BCP-47 tag or a primary subtag,
|
|
8970
|
-
* defaulting to `navigator.language`, then to 'en'
|
|
8971
|
-
*
|
|
9053
|
+
* defaulting to `navigator.language`, then to 'en', an empty one included.
|
|
9054
|
+
* It is canonicalized (`IT-it` and `it_IT` both read `it-IT`), and one that
|
|
9055
|
+
* is no language tag at all throws here, rather than at the first message
|
|
9056
|
+
* or number formatted in it. Its primary subtag picks the built-in
|
|
9057
|
+
* translations (en, it, es, fr); the messages
|
|
8972
9058
|
* are the english ones, overridden by that language's, overridden by
|
|
8973
9059
|
* `translations`, so a language without built-in translations reads in
|
|
8974
9060
|
* english. `translations` is a flat map of keys to messages: reword built-in
|
|
@@ -8976,9 +9062,18 @@ class Plugin {
|
|
|
8976
9062
|
* `httpClient` is the client every ful component fetches through, registered
|
|
8977
9063
|
* as the `http-client` component; without one, `configure` builds a client
|
|
8978
9064
|
* that sends the csrf token and navigates to '/' on a 401 response.
|
|
9065
|
+
* @throws {RangeError} when `language` is not a language tag
|
|
8979
9066
|
*/
|
|
8980
9067
|
constructor(options = {}) {
|
|
8981
|
-
|
|
9068
|
+
const language = options.language || navigator?.language || 'en';
|
|
9069
|
+
try {
|
|
9070
|
+
// unicode locale identifiers separate subtags with an underscore too, Intl takes the dash alone
|
|
9071
|
+
this.#language = Intl.getCanonicalLocales(String(language).replaceAll('_', '-'))[0];
|
|
9072
|
+
} catch {
|
|
9073
|
+
throw new RangeError(
|
|
9074
|
+
`Plugin: language '${language}' is not a language tag: pass a BCP-47 tag such as 'it' or 'it-IT', or leave it out for the browser's language`,
|
|
9075
|
+
);
|
|
9076
|
+
}
|
|
8982
9077
|
this.#translations = options.translations ?? {};
|
|
8983
9078
|
this.#httpClient = options.httpClient ?? null;
|
|
8984
9079
|
}
|
|
@@ -8987,7 +9082,7 @@ class Plugin {
|
|
|
8987
9082
|
* Defines on `registry` the `l10n` module, the `http-client` component,
|
|
8988
9083
|
* every `ful-*` element, the `loaders:select`, `loaders:form` and
|
|
8989
9084
|
* `loaders:table` components, and an overlay publishing `l10n` (the merged
|
|
8990
|
-
* messages) and `locale` (the `language
|
|
9085
|
+
* messages) and `locale` (the `language`, canonicalized) to every template.
|
|
8991
9086
|
* @param {{ defineModule(name: string, module: any): any, defineComponent(name: string, component: any): any, defineElement(tag: string, klass: CustomElementConstructor): any, defineOverlay(...data: any[]): any }} registry
|
|
8992
9087
|
* the ftl `Registry`, each of those methods answering the registry itself
|
|
8993
9088
|
* @returns {void}
|
|
@@ -8995,7 +9090,7 @@ class Plugin {
|
|
|
8995
9090
|
configure(registry) {
|
|
8996
9091
|
const httpClient =
|
|
8997
9092
|
this.#httpClient ?? HttpClient.builder().withCsrfToken().withRedirectOnUnauthorized('/').build();
|
|
8998
|
-
const language = this.#language.split('-')[0]
|
|
9093
|
+
const language = this.#language.split('-')[0];
|
|
8999
9094
|
const l10n = { ...BUILTIN.en, ...BUILTIN[language], ...this.#translations };
|
|
9000
9095
|
registry
|
|
9001
9096
|
.defineModule('l10n', Localization)
|
|
@@ -9039,5 +9134,5 @@ class Plugin {
|
|
|
9039
9134
|
}
|
|
9040
9135
|
}
|
|
9041
9136
|
|
|
9042
|
-
export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, Menu, NumberFilter, PageLeave, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader,
|
|
9137
|
+
export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, Menu, NumberFilter, PageLeave, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
|
|
9043
9138
|
//# sourceMappingURL=ful.mjs.map
|