@optionfactory/fml 9.0.0-rc30 → 9.0.0-rc32
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 +15 -1
- package/dist/fml.css +6 -6
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +910 -105
- 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 +125 -62
- package/dist/ful.iife.js +821 -79
- 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 +823 -80
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +9 -1
- package/dist/web-types.json +18 -2
- package/package.json +2 -2
package/dist/ful.iife.js
CHANGED
|
@@ -798,12 +798,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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
|
index_mjs.Attributes.set(this.#announces ?? this.#control, 'aria-invalid', error ? 'true' : null);
|
|
993
1007
|
if (!error) {
|
|
994
1008
|
this.internals.setValidity({});
|
|
@@ -1199,11 +1213,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 && index_mjs.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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
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 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4748
4784
|
radioEls.forEach((el) => {
|
|
4749
4785
|
el.remove();
|
|
4750
4786
|
});
|
|
4787
|
+
if (index_mjs.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]');
|
|
@@ -4889,6 +4928,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4889
4928
|
* @typedef {{ data: any[], size: number }} TablePageResponse the rows of the
|
|
4890
4929
|
* requested page and the total number of rows across every page
|
|
4891
4930
|
*/
|
|
4931
|
+
/**
|
|
4932
|
+
* @typedef {{ count: Element, appending: Element, failure: Element, error: Element }} TableStatus the pieces
|
|
4933
|
+
* of the status bar `paging="infinite"` and `paging="virtual"` show in place of the pager
|
|
4934
|
+
*/
|
|
4892
4935
|
/**
|
|
4893
4936
|
* @typedef {{ load(pageRequest: TablePageRequest, sortRequest: TableSortRequest|null, filterRequest: Record<string, any>): Promise<TablePageResponse> }} TableRowLoader
|
|
4894
4937
|
*/
|
|
@@ -4903,6 +4946,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4903
4946
|
* current `order` in the cycle `asc`, `desc`, `null`. The button does not
|
|
4904
4947
|
* change its own `order`: whoever handles the event sets it, which `ful-table`
|
|
4905
4948
|
* does once the sorted page has loaded.
|
|
4949
|
+
*
|
|
4950
|
+
* The stylesheet draws an up arrow for `asc` and a down arrow for `desc`. An
|
|
4951
|
+
* unsorted button shows the up arrow dimmed, `asc` being the order its first
|
|
4952
|
+
* activation asks for.
|
|
4906
4953
|
*/
|
|
4907
4954
|
class SortButton extends index_mjs.ParsedElement {
|
|
4908
4955
|
static attributes = ['sorter'];
|
|
@@ -5357,6 +5404,21 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5357
5404
|
* for it. Without it the table shows an initial panel until `load`,
|
|
5358
5405
|
* `reload` or `resetWithFilter` is called.
|
|
5359
5406
|
*
|
|
5407
|
+
* `paging="infinite"`, read once at the upgrade, replaces the pager with a
|
|
5408
|
+
* status bar saying how many rows of how many are shown, with a reload
|
|
5409
|
+
* button. A load still replaces the rows with one page, scrolling the rows
|
|
5410
|
+
* back to the top; from then on, whenever a row of the last half page
|
|
5411
|
+
* comes into view, the next page is asked with the same size, sort and
|
|
5412
|
+
* filters and appended (see `load`). `reload`, a sort, a filter submit and a
|
|
5413
|
+
* size change start over from the first page.
|
|
5414
|
+
*
|
|
5415
|
+
* `paging="virtual"` shows the same status bar, saying which rows of how many
|
|
5416
|
+
* are in view, and keeps in the page only the page in view and one on either
|
|
5417
|
+
* side of it, the others standing in as empty space sized on the height of
|
|
5418
|
+
* the rows. The rows must scroll inside the table, as `ful-fill` makes them:
|
|
5419
|
+
* a table whose rows the page scrolls appends as `paging="infinite"` does,
|
|
5420
|
+
* and warns. See `load` and `reload`.
|
|
5421
|
+
*
|
|
5360
5422
|
* Inside the table, and not inside a `ful-table` nested in it, a
|
|
5361
5423
|
* `page:requested` loads that page with the current size,
|
|
5362
5424
|
* sort and filters; a `sort:requested` loads the current page with the new
|
|
@@ -5372,13 +5434,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5372
5434
|
*/
|
|
5373
5435
|
class Table extends index_mjs.ParsedElement {
|
|
5374
5436
|
/** Read once at the upgrade. */
|
|
5375
|
-
static attributes = ['loader', 'autoload:presence'];
|
|
5437
|
+
static attributes = ['loader', 'autoload:presence', 'paging'];
|
|
5376
5438
|
/** These stay live after the upgrade: see `pageSize`, `src` and `method`. */
|
|
5377
5439
|
static observed = ['page-size:number', 'src', 'method'];
|
|
5378
5440
|
static slots = true;
|
|
5379
5441
|
static config = {
|
|
5380
5442
|
searchIcon: 'search',
|
|
5381
5443
|
emptyIcon: 'inbox',
|
|
5444
|
+
reloadIcon: 'arrow-clockwise',
|
|
5382
5445
|
};
|
|
5383
5446
|
static template = `
|
|
5384
5447
|
<ful-form data-tpl-if="slots.filters">
|
|
@@ -5388,7 +5451,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5388
5451
|
<table>
|
|
5389
5452
|
<caption data-tpl-if="slots.caption">{{{{ slots.caption }}}}</caption>
|
|
5390
5453
|
<thead></thead>
|
|
5454
|
+
<tbody data-tpl-if="virtual" data-ref="before" aria-hidden="true"><tr><td data-tpl-colspan="schema.length"></td></tr></tbody>
|
|
5391
5455
|
<tbody></tbody>
|
|
5456
|
+
<tbody data-tpl-if="virtual" data-ref="after" aria-hidden="true"><tr><td data-tpl-colspan="schema.length"></td></tr></tbody>
|
|
5392
5457
|
<tbody data-ref="initial">
|
|
5393
5458
|
<tr>
|
|
5394
5459
|
<td data-tpl-colspan="schema.length">
|
|
@@ -5432,7 +5497,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5432
5497
|
</tfoot>
|
|
5433
5498
|
</table>
|
|
5434
5499
|
</ful-table-wrapper>
|
|
5435
|
-
<ful-pagination current="0" total="1"></ful-pagination>
|
|
5500
|
+
<ful-pagination data-tpl-if="!scrolled" current="0" total="1"></ful-pagination>
|
|
5501
|
+
<ful-table-status data-tpl-if="scrolled">
|
|
5502
|
+
<span data-ref="count"></span>
|
|
5503
|
+
<ful-spinner data-ref="appending" role="status" hidden><span class="ful-sr-only">{{ #l10n:t('spinner.loading') }}</span></ful-spinner>
|
|
5504
|
+
<div data-ref="append-failure" role="alert" hidden>
|
|
5505
|
+
<span>{{ #l10n:t('table.error') }} <span data-ref="append-error"></span></span>
|
|
5506
|
+
<button type="button" data-ref="retry">{{ #l10n:t('table.retry') }}</button>
|
|
5507
|
+
</div>
|
|
5508
|
+
<button type="button" data-ref="reload" data-tpl-aria-label="#l10n:t('pagination.reload')"><ful-icon data-tpl-name="config.reloadIcon" aria-hidden="true"></ful-icon></button>
|
|
5509
|
+
</ful-table-status>
|
|
5436
5510
|
`;
|
|
5437
5511
|
static templates = {
|
|
5438
5512
|
row: `
|
|
@@ -5450,6 +5524,31 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5450
5524
|
#feedback;
|
|
5451
5525
|
#paginator;
|
|
5452
5526
|
#sorters;
|
|
5527
|
+
#wrapper;
|
|
5528
|
+
/** @type {TableStatus|null} null under the pager */
|
|
5529
|
+
#status = null;
|
|
5530
|
+
/** @type {IntersectionObserver|null} */
|
|
5531
|
+
#endWatch = null;
|
|
5532
|
+
/** @type {Element[]} the rows whose coming into view appends the next page, none while no append is awaited */
|
|
5533
|
+
#watched = [];
|
|
5534
|
+
/** the page the next append asks for, the rows shown, the total the last answer reported and the rows it carried */
|
|
5535
|
+
#appended = { next: 1, shown: 0, total: 0, last: 0 };
|
|
5536
|
+
/** whether the table keeps a window of pages, `paging="virtual"` over rows that scroll inside it */
|
|
5537
|
+
#virtual = false;
|
|
5538
|
+
/** @type {{ before: HTMLElement, after: HTMLElement } | null} the cells standing in for the rows above and below the window */
|
|
5539
|
+
#spacers = null;
|
|
5540
|
+
/** @type {{ first: number, last: number, pages: Map<number, Element[]> } | null} the pages in the page, in order, null while no rows show */
|
|
5541
|
+
#window = null;
|
|
5542
|
+
/** the total number of rows the last answer reported */
|
|
5543
|
+
#total = 0;
|
|
5544
|
+
/** the height of a row the spacers are sized on, measured on the first rows a load shows; 0 until measured */
|
|
5545
|
+
#rowHeight = 0;
|
|
5546
|
+
/** @type {{ readonly stale: boolean } | null} the claim of the window shift in flight */
|
|
5547
|
+
#shifting = null;
|
|
5548
|
+
/** whether the last window shift failed: scrolling asks for nothing more until a retry or a load */
|
|
5549
|
+
#windowFailed = false;
|
|
5550
|
+
/** @type {ReturnType<typeof setTimeout>|undefined} */
|
|
5551
|
+
#jump = undefined;
|
|
5453
5552
|
/** @type {{ pageRequest: { page: number, size: number }, sortRequest: any, filterRequest: any }} */
|
|
5454
5553
|
#latestRequest = { pageRequest: { page: 0, size: 10 }, sortRequest: null, filterRequest: {} };
|
|
5455
5554
|
/** whether a load has been asked for, by autoload or by a caller */
|
|
@@ -5479,18 +5578,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5479
5578
|
* writing the size is not a request to start loading, which is what
|
|
5480
5579
|
* `autoload` and `reload()` are for. A write that does not change the size
|
|
5481
5580
|
* does nothing. A write made before the render is replaced by the declared
|
|
5482
|
-
* `page-size` when the table renders
|
|
5581
|
+
* `page-size` when the table renders, or by ten when that is not a number
|
|
5582
|
+
* above zero.
|
|
5483
5583
|
*
|
|
5484
5584
|
* The reload is not awaited: a failure shows the error panel and its
|
|
5485
5585
|
* rejection is unhandled.
|
|
5486
5586
|
* @param {number|string|null|undefined} value a number or a string of
|
|
5487
5587
|
* one, such as a select's value; absent or null is the default of ten,
|
|
5488
5588
|
* so removing the attribute restores it, and a value that is not a
|
|
5489
|
-
* number is ignored
|
|
5589
|
+
* finite number above zero is ignored
|
|
5490
5590
|
*/
|
|
5491
5591
|
set pageSize(value) {
|
|
5492
5592
|
const size = Number(value ?? 10);
|
|
5493
|
-
if (Number.
|
|
5593
|
+
if (!Number.isFinite(size) || size <= 0 || size === this.#latestRequest.pageRequest.size) {
|
|
5494
5594
|
return;
|
|
5495
5595
|
}
|
|
5496
5596
|
this.#latestRequest = { ...this.#latestRequest, pageRequest: { page: 0, size } };
|
|
@@ -5576,6 +5676,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5576
5676
|
}
|
|
5577
5677
|
this.#buildLoader();
|
|
5578
5678
|
this.#latestRequest = { ...this.#latestRequest, pageRequest: { ...this.#latestRequest.pageRequest, page: 0 } };
|
|
5679
|
+
this.#window = null;
|
|
5579
5680
|
return true;
|
|
5580
5681
|
}
|
|
5581
5682
|
/**
|
|
@@ -5586,14 +5687,24 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5586
5687
|
async render({ slots }) {
|
|
5587
5688
|
const template = this.template();
|
|
5588
5689
|
const schema = TableSchemaParser.parse(slots.schema, template);
|
|
5589
|
-
const
|
|
5690
|
+
const paging = this.declared('paging');
|
|
5691
|
+
const fragment = template
|
|
5692
|
+
.withOverlay({
|
|
5693
|
+
slots,
|
|
5694
|
+
schema,
|
|
5695
|
+
scrolled: paging === 'infinite' || paging === 'virtual',
|
|
5696
|
+
virtual: paging === 'virtual',
|
|
5697
|
+
})
|
|
5698
|
+
.render();
|
|
5590
5699
|
const tableWrapper = /** @type HTMLTableElement */ (index_mjs.Nodes.queryChildren(fragment, 'ful-table-wrapper'));
|
|
5591
5700
|
const table = /** @type HTMLTableElement */ (tableWrapper.querySelector('table'));
|
|
5592
5701
|
index_mjs.Attributes.forward('table-', this, table);
|
|
5593
5702
|
this.#buildLoader();
|
|
5594
5703
|
|
|
5704
|
+
this.#wrapper = tableWrapper;
|
|
5705
|
+
const status = index_mjs.Nodes.queryChildren(fragment, 'ful-table-status');
|
|
5595
5706
|
this.#schema = schema;
|
|
5596
|
-
this.#body = table.querySelector(':scope > tbody');
|
|
5707
|
+
this.#body = table.querySelector(':scope > tbody:not([data-ref])');
|
|
5597
5708
|
this.#loading = table.querySelector(':scope > tbody[data-ref=loading]');
|
|
5598
5709
|
this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
|
|
5599
5710
|
this.#empty = table.querySelector(':scope > tbody[data-ref=empty]');
|
|
@@ -5603,16 +5714,67 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5603
5714
|
this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
|
|
5604
5715
|
this.#paginator = index_mjs.Nodes.queryChildren(fragment, 'ful-pagination');
|
|
5605
5716
|
this.replaceChildren(fragment);
|
|
5717
|
+
if (paging === 'virtual') {
|
|
5718
|
+
const overflow = getComputedStyle(tableWrapper).overflowY;
|
|
5719
|
+
this.#virtual = overflow === 'auto' || overflow === 'scroll';
|
|
5720
|
+
if (!this.#virtual) {
|
|
5721
|
+
console.warn(
|
|
5722
|
+
'ful-table: paging="virtual" needs rows that scroll inside the table, as ful-fill makes them; this one appends its pages as paging="infinite" does',
|
|
5723
|
+
);
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
if (status) {
|
|
5727
|
+
const piece = (/** @type string */ ref) =>
|
|
5728
|
+
/** @type Element */ (status.querySelector(`[data-ref=${ref}]`));
|
|
5729
|
+
this.#status = {
|
|
5730
|
+
count: piece('count'),
|
|
5731
|
+
appending: piece('appending'),
|
|
5732
|
+
failure: piece('append-failure'),
|
|
5733
|
+
error: piece('append-error'),
|
|
5734
|
+
};
|
|
5735
|
+
piece('reload').addEventListener('click', () => this.reload());
|
|
5736
|
+
piece('retry').addEventListener('click', () => {
|
|
5737
|
+
if (!this.#virtual) {
|
|
5738
|
+
this.#appendNext();
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5741
|
+
this.#windowFailed = false;
|
|
5742
|
+
this.#reconcile(true);
|
|
5743
|
+
});
|
|
5744
|
+
}
|
|
5745
|
+
if (this.#virtual) {
|
|
5746
|
+
this.#spacers = {
|
|
5747
|
+
before: /** @type HTMLElement */ (table.querySelector(':scope > tbody[data-ref=before] td')),
|
|
5748
|
+
after: /** @type HTMLElement */ (table.querySelector(':scope > tbody[data-ref=after] td')),
|
|
5749
|
+
};
|
|
5750
|
+
// scroll fires once per frame at most, so it needs no throttle of its own
|
|
5751
|
+
tableWrapper.addEventListener(
|
|
5752
|
+
'scroll',
|
|
5753
|
+
() => {
|
|
5754
|
+
this.#countInView();
|
|
5755
|
+
this.#reconcile(false);
|
|
5756
|
+
},
|
|
5757
|
+
{ passive: true },
|
|
5758
|
+
);
|
|
5759
|
+
} else if (status) {
|
|
5760
|
+
this.#endWatch = new IntersectionObserver((entries) => {
|
|
5761
|
+
// an entry queued before the watch moved on to another row, or stopped, asks for nothing
|
|
5762
|
+
if (entries.some((e) => e.isIntersecting && this.#watched.includes(e.target))) {
|
|
5763
|
+
this.#appendNext();
|
|
5764
|
+
}
|
|
5765
|
+
});
|
|
5766
|
+
}
|
|
5606
5767
|
const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
|
|
5607
5768
|
schema.headersTemplate.renderTo(thead);
|
|
5608
5769
|
this.#sorters = thead.querySelectorAll('ful-sorter');
|
|
5609
5770
|
await index_mjs.Rendering.waitForChildren(this);
|
|
5610
5771
|
|
|
5611
5772
|
const maybeForm = /** @type any */ (index_mjs.Nodes.queryChildren(this, 'ful-form'));
|
|
5773
|
+
const declaredSize = this.declared('page-size');
|
|
5612
5774
|
this.#latestRequest = {
|
|
5613
5775
|
pageRequest: {
|
|
5614
5776
|
page: 0,
|
|
5615
|
-
size:
|
|
5777
|
+
size: Number.isFinite(declaredSize) && declaredSize > 0 ? declaredSize : 10,
|
|
5616
5778
|
},
|
|
5617
5779
|
sortRequest: schema.sort,
|
|
5618
5780
|
filterRequest: maybeForm?.values ?? {},
|
|
@@ -5658,6 +5820,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5658
5820
|
* `submit` listeners as the later ones, and the load its `submit:success`
|
|
5659
5821
|
* starts is the one awaited. A filter submit that is cancelled, fails or
|
|
5660
5822
|
* is dropped for one in flight loads nothing, and the promise resolves.
|
|
5823
|
+
*
|
|
5824
|
+
* Under `paging="virtual"`, once rows show, it asks again for the pages in
|
|
5825
|
+
* the page, with the same size, sort and filters, and shows them where
|
|
5826
|
+
* they are, the scroll position and the widths of the columns kept; the
|
|
5827
|
+
* table is busy and settles, fails and dispatches as a load does, one
|
|
5828
|
+
* `load:success` per page. When the answers report fewer rows than the
|
|
5829
|
+
* window starts at, it loads the first page instead. A `src` or `method`
|
|
5830
|
+
* write still starts over from the first page.
|
|
5661
5831
|
* @returns {Promise<void>} as `load`
|
|
5662
5832
|
*/
|
|
5663
5833
|
async reload() {
|
|
@@ -5667,6 +5837,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5667
5837
|
}
|
|
5668
5838
|
return;
|
|
5669
5839
|
}
|
|
5840
|
+
if (this.#window) {
|
|
5841
|
+
return await this.#reloadWindow();
|
|
5842
|
+
}
|
|
5670
5843
|
return await this.load(
|
|
5671
5844
|
this.#latestRequest.pageRequest,
|
|
5672
5845
|
this.#latestRequest.sortRequest,
|
|
@@ -5696,12 +5869,49 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5696
5869
|
* `aria-busy` is removed, `load:failure` is dispatched (bubbling, not
|
|
5697
5870
|
* cancelable) with `detail` set to
|
|
5698
5871
|
* `{ pageRequest, sortRequest, filterRequest, exception }`, and the
|
|
5699
|
-
* returned promise rejects with the same error.
|
|
5872
|
+
* returned promise rejects with the same error. An answer the table cannot
|
|
5873
|
+
* page fails the same way, with an error saying so: one whose `size` is
|
|
5874
|
+
* not a number, as a response of another shape read without a
|
|
5875
|
+
* `response-mapper` is, or a page size of zero.
|
|
5700
5876
|
*
|
|
5701
5877
|
* A load started after this one supersedes it: when this one's answer or
|
|
5702
5878
|
* failure arrives it changes nothing, dispatches nothing and resolves;
|
|
5703
5879
|
* superseded while its rows render, it leaves them to the newer load and
|
|
5704
5880
|
* dispatches nothing.
|
|
5881
|
+
*
|
|
5882
|
+
* Under `paging="infinite"` the status bar takes the pager's place, and
|
|
5883
|
+
* once the load has dispatched `load:success` the next page is awaited:
|
|
5884
|
+
* when a row of the last half page comes into view, through
|
|
5885
|
+
* whatever scrolls it, the following page is asked for with the same
|
|
5886
|
+
* size, sort and filters and its rows are appended. An append runs
|
|
5887
|
+
* without `aria-busy`, a spinner in the status bar standing in, and
|
|
5888
|
+
* dispatches `load:success` or `load:failure` as a load does, its
|
|
5889
|
+
* `pageRequest` naming the appended page. A failed append keeps the
|
|
5890
|
+
* rows, shows the error in the status bar with a retry button and
|
|
5891
|
+
* rejects to nobody, nothing awaiting it. Appending stops once the rows
|
|
5892
|
+
* shown reach the total the last answer reported, or an answer carries
|
|
5893
|
+
* no rows. A load supersedes an append in flight; `reload` repeats this
|
|
5894
|
+
* load, not the pages appended to it.
|
|
5895
|
+
*
|
|
5896
|
+
* Under `paging="virtual"` the load shows its page as the window, scrolled
|
|
5897
|
+
* to where it sits among the total, and measures two things on its rows:
|
|
5898
|
+
* the height the empty space standing in for the other rows is sized on,
|
|
5899
|
+
* and the width of the columns, which then stay as they are, in
|
|
5900
|
+
* proportion, until the next load, so that the pages coming and going do
|
|
5901
|
+
* not move them. From then on the window follows the scroll: the page in
|
|
5902
|
+
* view and one on either side stay in the page and the others are dropped,
|
|
5903
|
+
* the page holding the focus kept while it is within two pages of them. A
|
|
5904
|
+
* page next to the window is asked for at once; a scroll landing farther
|
|
5905
|
+
* waits for the scroll to rest. Window pages are asked for without
|
|
5906
|
+
* `aria-busy`, a spinner in the status bar standing in, and dispatch
|
|
5907
|
+
* `load:success` or `load:failure` per page as appends do; a failure keeps
|
|
5908
|
+
* the rows and offers a retry. The row in view stays where it is as pages
|
|
5909
|
+
* come and go above it: the table measures it before and after and
|
|
5910
|
+
* corrects the scroll by whatever the browser's own scroll anchoring has
|
|
5911
|
+
* not kept, which is all of it where the browser anchors no table rows
|
|
5912
|
+
* (Firefox), has no scroll anchoring or the rows' container declares
|
|
5913
|
+
* `overflow-anchor: none`, and nothing where it has. The table carries
|
|
5914
|
+
* `aria-rowcount` and each row its `aria-rowindex`.
|
|
5705
5915
|
* @param {TablePageRequest} pageRequest
|
|
5706
5916
|
* @param {TableSortRequest|null} sortRequest
|
|
5707
5917
|
* @param {Record<string, any>} filterRequest
|
|
@@ -5712,6 +5922,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5712
5922
|
this.#loadRequested = true;
|
|
5713
5923
|
this.#requestedFilters = filterRequest;
|
|
5714
5924
|
const claim = this.#loads.take();
|
|
5925
|
+
this.#watched = [];
|
|
5926
|
+
this.#endWatch?.disconnect();
|
|
5927
|
+
clearTimeout(this.#jump);
|
|
5928
|
+
this.#shifting = null;
|
|
5929
|
+
this.#windowFailed = false;
|
|
5930
|
+
this.#status?.appending.setAttribute('hidden', '');
|
|
5931
|
+
this.#status?.failure.setAttribute('hidden', '');
|
|
5715
5932
|
this.#loading.toggleAttribute('hidden', this.#body.childElementCount > 0);
|
|
5716
5933
|
this.#feedback.setAttribute('hidden', '');
|
|
5717
5934
|
this.#noAutoload.setAttribute('hidden', '');
|
|
@@ -5724,6 +5941,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5724
5941
|
return;
|
|
5725
5942
|
}
|
|
5726
5943
|
lastPage = Math.max(0, Math.ceil(pageResponse.size / pageRequest.size) - 1);
|
|
5944
|
+
if (!Number.isFinite(lastPage)) {
|
|
5945
|
+
throw new Error(
|
|
5946
|
+
`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`,
|
|
5947
|
+
);
|
|
5948
|
+
}
|
|
5727
5949
|
if (pageRequest.page <= lastPage) {
|
|
5728
5950
|
this.#latestRequest = { pageRequest, sortRequest, filterRequest };
|
|
5729
5951
|
this.#loaded = true;
|
|
@@ -5736,6 +5958,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5736
5958
|
if (claim.stale) {
|
|
5737
5959
|
return;
|
|
5738
5960
|
}
|
|
5961
|
+
if (this.#virtual) {
|
|
5962
|
+
this.#settleWindow();
|
|
5963
|
+
}
|
|
5739
5964
|
this.removeAttribute('aria-busy');
|
|
5740
5965
|
this.dispatchEvent(
|
|
5741
5966
|
new CustomEvent('load:success', {
|
|
@@ -5744,24 +5969,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5744
5969
|
detail: { pageRequest, sortRequest, filterRequest, response: pageResponse },
|
|
5745
5970
|
}),
|
|
5746
5971
|
);
|
|
5972
|
+
this.#watchEnd(pageRequest.size);
|
|
5973
|
+
this.#reconcile(false);
|
|
5747
5974
|
return;
|
|
5748
5975
|
}
|
|
5749
5976
|
} catch (/** @type any */ error) {
|
|
5750
5977
|
if (claim.stale) {
|
|
5751
5978
|
return;
|
|
5752
5979
|
}
|
|
5753
|
-
this.#
|
|
5754
|
-
this.#body.replaceChildren();
|
|
5755
|
-
this.#feedback.removeAttribute('hidden');
|
|
5756
|
-
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = index_mjs.Localization.of().failure(error);
|
|
5757
|
-
this.removeAttribute('aria-busy');
|
|
5758
|
-
this.dispatchEvent(
|
|
5759
|
-
new CustomEvent('load:failure', {
|
|
5760
|
-
bubbles: true,
|
|
5761
|
-
cancelable: false,
|
|
5762
|
-
detail: { pageRequest, sortRequest, filterRequest, exception: error },
|
|
5763
|
-
}),
|
|
5764
|
-
);
|
|
5980
|
+
this.#fail(error, { pageRequest, sortRequest, filterRequest });
|
|
5765
5981
|
throw error;
|
|
5766
5982
|
}
|
|
5767
5983
|
return await this.load({ page: lastPage, size: pageRequest.size }, sortRequest, filterRequest);
|
|
@@ -5792,19 +6008,471 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5792
6008
|
filterRequest,
|
|
5793
6009
|
);
|
|
5794
6010
|
}
|
|
5795
|
-
|
|
5796
|
-
|
|
6011
|
+
/**
|
|
6012
|
+
* Watches the last half page of rows, so that any of them coming into
|
|
6013
|
+
* view appends the next page, unless the rows shown already reach the
|
|
6014
|
+
* total or the last answer carried none. A single row would be missed by
|
|
6015
|
+
* a scroll jumping past it, to the end for instance.
|
|
6016
|
+
* @param {number} size the rows a page asks for
|
|
6017
|
+
*/
|
|
6018
|
+
#watchEnd(size) {
|
|
6019
|
+
const { shown, total, last } = this.#appended;
|
|
6020
|
+
if (!this.#endWatch || !(shown < total) || last === 0) {
|
|
6021
|
+
return;
|
|
6022
|
+
}
|
|
6023
|
+
const rows = [...this.#body.children];
|
|
6024
|
+
this.#watched = rows.slice(Math.max(0, rows.length - Math.ceil(size / 2)));
|
|
6025
|
+
for (const row of this.#watched) {
|
|
6026
|
+
this.#endWatch.observe(row);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
async #appendNext() {
|
|
6030
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
6031
|
+
this.#watched = [];
|
|
6032
|
+
this.#endWatch?.disconnect();
|
|
6033
|
+
const { sortRequest, filterRequest } = this.#latestRequest;
|
|
6034
|
+
const pageRequest = { page: this.#appended.next, size: this.#latestRequest.pageRequest.size };
|
|
6035
|
+
const claim = this.#loads.take();
|
|
6036
|
+
status.failure.setAttribute('hidden', '');
|
|
6037
|
+
status.appending.removeAttribute('hidden');
|
|
6038
|
+
try {
|
|
6039
|
+
const pageResponse = await this.#loader.load(pageRequest, sortRequest, filterRequest);
|
|
6040
|
+
if (claim.stale) {
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
this.#body.append(
|
|
6044
|
+
this.template('row')
|
|
6045
|
+
.withOverlay({ schema: this.#schema, pageRequest, filterRequest, pageResponse })
|
|
6046
|
+
.render(),
|
|
6047
|
+
);
|
|
6048
|
+
this.#appended = {
|
|
6049
|
+
next: pageRequest.page + 1,
|
|
6050
|
+
shown: this.#appended.shown + pageResponse.data.length,
|
|
6051
|
+
total: pageResponse.size,
|
|
6052
|
+
last: pageResponse.data.length,
|
|
6053
|
+
};
|
|
6054
|
+
status.count.textContent = index_mjs.Localization.of().t('table.loaded', {
|
|
6055
|
+
loaded: this.#appended.shown,
|
|
6056
|
+
total: this.#appended.total,
|
|
6057
|
+
});
|
|
6058
|
+
await index_mjs.Rendering.waitForChildren(this.#body).catch(() => undefined);
|
|
6059
|
+
if (claim.stale) {
|
|
6060
|
+
return;
|
|
6061
|
+
}
|
|
6062
|
+
status.appending.setAttribute('hidden', '');
|
|
6063
|
+
this.dispatchEvent(
|
|
6064
|
+
new CustomEvent('load:success', {
|
|
6065
|
+
bubbles: true,
|
|
6066
|
+
cancelable: false,
|
|
6067
|
+
detail: { pageRequest, sortRequest, filterRequest, response: pageResponse },
|
|
6068
|
+
}),
|
|
6069
|
+
);
|
|
6070
|
+
this.#watchEnd(pageRequest.size);
|
|
6071
|
+
} catch (/** @type any */ error) {
|
|
6072
|
+
if (claim.stale) {
|
|
6073
|
+
return;
|
|
6074
|
+
}
|
|
6075
|
+
status.appending.setAttribute('hidden', '');
|
|
6076
|
+
status.error.textContent = index_mjs.Localization.of().failure(error);
|
|
6077
|
+
status.failure.removeAttribute('hidden');
|
|
6078
|
+
this.dispatchEvent(
|
|
6079
|
+
new CustomEvent('load:failure', {
|
|
6080
|
+
bubbles: true,
|
|
6081
|
+
cancelable: false,
|
|
6082
|
+
detail: { pageRequest, sortRequest, filterRequest, exception: error },
|
|
6083
|
+
}),
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
/**
|
|
6088
|
+
* Renders the rows of a page, numbering them with `aria-rowindex` under
|
|
6089
|
+
* `paging="virtual"`, the header row being the first.
|
|
6090
|
+
* @param {TablePageRequest} pageRequest
|
|
6091
|
+
* @param {Record<string, any>} filterRequest
|
|
6092
|
+
* @param {TablePageResponse} pageResponse
|
|
6093
|
+
* @returns {Element[]}
|
|
6094
|
+
*/
|
|
6095
|
+
#pageRows(pageRequest, filterRequest, pageResponse) {
|
|
6096
|
+
const rows = [
|
|
6097
|
+
...this.template('row')
|
|
6098
|
+
.withOverlay({ schema: this.#schema, pageRequest, filterRequest, pageResponse })
|
|
6099
|
+
.render().children,
|
|
6100
|
+
];
|
|
6101
|
+
if (this.#virtual) {
|
|
6102
|
+
rows.forEach((row, i) => {
|
|
6103
|
+
row.setAttribute('aria-rowindex', String(pageRequest.page * pageRequest.size + i + 2));
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6106
|
+
return rows;
|
|
6107
|
+
}
|
|
6108
|
+
/**
|
|
6109
|
+
* Shows a failed load: the rows go, the error panel shows and
|
|
6110
|
+
* `load:failure` is dispatched.
|
|
6111
|
+
* @param {any} error
|
|
6112
|
+
* @param {{ pageRequest: TablePageRequest, sortRequest: TableSortRequest|null, filterRequest: Record<string, any> }} request
|
|
6113
|
+
*/
|
|
6114
|
+
#fail(error, request) {
|
|
5797
6115
|
this.#loading.setAttribute('hidden', '');
|
|
5798
|
-
this.#body.replaceChildren(
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
6116
|
+
this.#body.replaceChildren();
|
|
6117
|
+
this.#window = null;
|
|
6118
|
+
this.#fitSpacers();
|
|
6119
|
+
this.#status?.count.replaceChildren();
|
|
6120
|
+
this.#feedback.removeAttribute('hidden');
|
|
6121
|
+
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = index_mjs.Localization.of().failure(error);
|
|
6122
|
+
this.removeAttribute('aria-busy');
|
|
6123
|
+
this.dispatchEvent(
|
|
6124
|
+
new CustomEvent('load:failure', {
|
|
6125
|
+
bubbles: true,
|
|
6126
|
+
cancelable: false,
|
|
6127
|
+
detail: { ...request, exception: error },
|
|
6128
|
+
}),
|
|
5807
6129
|
);
|
|
6130
|
+
}
|
|
6131
|
+
/** Counts the rows of a load just shown and moves the window where its page sits. */
|
|
6132
|
+
#settleWindow() {
|
|
6133
|
+
const table = /** @type HTMLTableElement */ (this.#body.parentElement);
|
|
6134
|
+
table.setAttribute('aria-rowcount', String(this.#total + 1));
|
|
6135
|
+
table.tHead?.rows[0]?.setAttribute('aria-rowindex', '1');
|
|
6136
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
6137
|
+
const window = this.#window;
|
|
6138
|
+
if (!window) {
|
|
6139
|
+
this.#fitSpacers();
|
|
6140
|
+
status.count.textContent = index_mjs.Localization.of().t('table.loaded', { loaded: 0, total: this.#total });
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
6143
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
6144
|
+
const from = window.first * size;
|
|
6145
|
+
status.count.textContent = index_mjs.Localization.of().t('table.range', {
|
|
6146
|
+
from: from + 1,
|
|
6147
|
+
to: from + /** @type Element[] */ (window.pages.get(window.first)).length,
|
|
6148
|
+
total: this.#total,
|
|
6149
|
+
});
|
|
6150
|
+
if (this.#measureRows()) {
|
|
6151
|
+
this.#wrapper.scrollTop = from * this.#rowHeight;
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
/**
|
|
6155
|
+
* Measures the height of a row and locks the widths of the columns, once
|
|
6156
|
+
* per load, on the rows it shows, then sizes the spacers. A table that is not laid out, inside a
|
|
6157
|
+
* closed disclosure for instance, measures nothing and is measured when
|
|
6158
|
+
* its rows next scroll.
|
|
6159
|
+
* @returns {boolean} whether the rows are measured
|
|
6160
|
+
*/
|
|
6161
|
+
#measureRows() {
|
|
6162
|
+
if (this.#rowHeight > 0) {
|
|
6163
|
+
return true;
|
|
6164
|
+
}
|
|
6165
|
+
const count = this.#body.childElementCount;
|
|
6166
|
+
const height = this.#body.getBoundingClientRect().height;
|
|
6167
|
+
if (count === 0 || height === 0) {
|
|
6168
|
+
return false;
|
|
6169
|
+
}
|
|
6170
|
+
this.#rowHeight = height / count;
|
|
6171
|
+
this.#lockColumns();
|
|
6172
|
+
this.#fitSpacers();
|
|
6173
|
+
return true;
|
|
6174
|
+
}
|
|
6175
|
+
/**
|
|
6176
|
+
* Fixes the widths of the columns, in proportion to the table, at those
|
|
6177
|
+
* the rows in the page give them.
|
|
6178
|
+
*/
|
|
6179
|
+
#lockColumns() {
|
|
6180
|
+
const table = /** @type HTMLTableElement */ (this.#body.parentElement);
|
|
6181
|
+
const widths = [...(table.tHead?.rows[0]?.cells ?? [])].map((cell) => cell.getBoundingClientRect().width);
|
|
6182
|
+
const sum = widths.reduce((a, b) => a + b, 0);
|
|
6183
|
+
const columns = document.createElement('colgroup');
|
|
6184
|
+
columns.setAttribute('data-ref', 'widths');
|
|
6185
|
+
for (const width of widths) {
|
|
6186
|
+
const column = document.createElement('col');
|
|
6187
|
+
column.style.width = `${(width / sum) * 100}%`;
|
|
6188
|
+
columns.append(column);
|
|
6189
|
+
}
|
|
6190
|
+
table.insertBefore(columns, table.tHead);
|
|
6191
|
+
}
|
|
6192
|
+
#unlockColumns() {
|
|
6193
|
+
this.#body.parentElement?.querySelector(':scope > colgroup[data-ref=widths]')?.remove();
|
|
6194
|
+
}
|
|
6195
|
+
/** Sizes the space standing in for the rows above and below the window. */
|
|
6196
|
+
#fitSpacers() {
|
|
6197
|
+
if (!this.#spacers) {
|
|
6198
|
+
return;
|
|
6199
|
+
}
|
|
6200
|
+
const window = this.#window;
|
|
6201
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
6202
|
+
const above = window ? window.first * size : 0;
|
|
6203
|
+
const below = window ? Math.max(0, this.#total - (window.last + 1) * size) : 0;
|
|
6204
|
+
this.#spacers.before.style.height = `${above * this.#rowHeight}px`;
|
|
6205
|
+
this.#spacers.after.style.height = `${below * this.#rowHeight}px`;
|
|
6206
|
+
}
|
|
6207
|
+
/** Says in the status bar which rows are in view; nothing changes while none is. */
|
|
6208
|
+
#countInView() {
|
|
6209
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
6210
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
6211
|
+
const view = this.#wrapper.getBoundingClientRect();
|
|
6212
|
+
const top = view.top + (this.#body.parentElement?.querySelector(':scope > thead')?.getBoundingClientRect().height ?? 0);
|
|
6213
|
+
let from = 0;
|
|
6214
|
+
let to = 0;
|
|
6215
|
+
for (const [page, rows] of this.#window?.pages ?? []) {
|
|
6216
|
+
rows.forEach((row, i) => {
|
|
6217
|
+
const rect = row.getBoundingClientRect();
|
|
6218
|
+
if (rect.bottom > top && rect.top < view.bottom) {
|
|
6219
|
+
from ||= page * size + i + 1;
|
|
6220
|
+
to = page * size + i + 1;
|
|
6221
|
+
}
|
|
6222
|
+
});
|
|
6223
|
+
}
|
|
6224
|
+
if (from > 0) {
|
|
6225
|
+
status.count.textContent = index_mjs.Localization.of().t('table.range', { from, to, total: this.#total });
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
/**
|
|
6229
|
+
* The page at the middle of the view: found among the rows in the page,
|
|
6230
|
+
* or estimated on the height of a row when the middle falls in the space
|
|
6231
|
+
* standing in for others.
|
|
6232
|
+
* @param {{ first: number, last: number, pages: Map<number, Element[]> }} window
|
|
6233
|
+
* @returns {number}
|
|
6234
|
+
*/
|
|
6235
|
+
#pageInView(window) {
|
|
6236
|
+
const pageHeight = this.#latestRequest.pageRequest.size * this.#rowHeight;
|
|
6237
|
+
const view = this.#wrapper.getBoundingClientRect();
|
|
6238
|
+
const middle = view.top + view.height / 2;
|
|
6239
|
+
const rows = this.#body.getBoundingClientRect();
|
|
6240
|
+
if (middle < rows.top) {
|
|
6241
|
+
return Math.max(0, window.first - Math.ceil((rows.top - middle) / pageHeight));
|
|
6242
|
+
}
|
|
6243
|
+
if (middle >= rows.bottom) {
|
|
6244
|
+
return window.last + 1 + Math.floor((middle - rows.bottom) / pageHeight);
|
|
6245
|
+
}
|
|
6246
|
+
let found = window.first;
|
|
6247
|
+
for (const [page, pageRows] of window.pages) {
|
|
6248
|
+
if (pageRows.length > 0 && pageRows[0].getBoundingClientRect().top <= middle) {
|
|
6249
|
+
found = page;
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
return found;
|
|
6253
|
+
}
|
|
6254
|
+
/**
|
|
6255
|
+
* Moves the window to the page in view and its neighbours. A window next
|
|
6256
|
+
* to the current one moves at once; one farther away waits for the scroll
|
|
6257
|
+
* to rest, unless `settled` says it has.
|
|
6258
|
+
* @param {boolean} settled
|
|
6259
|
+
*/
|
|
6260
|
+
#reconcile(settled) {
|
|
6261
|
+
const window = this.#window;
|
|
6262
|
+
const pageCount = Math.ceil(this.#total / this.#latestRequest.pageRequest.size);
|
|
6263
|
+
if (
|
|
6264
|
+
!window ||
|
|
6265
|
+
this.#shifting ||
|
|
6266
|
+
this.#windowFailed ||
|
|
6267
|
+
this.hasAttribute('aria-busy') ||
|
|
6268
|
+
!this.#wrapper.isConnected ||
|
|
6269
|
+
!(pageCount > 0) ||
|
|
6270
|
+
!this.#measureRows()
|
|
6271
|
+
) {
|
|
6272
|
+
return;
|
|
6273
|
+
}
|
|
6274
|
+
const target = Math.min(pageCount - 1, this.#pageInView(window));
|
|
6275
|
+
let first = Math.max(0, target - 1);
|
|
6276
|
+
let last = Math.min(pageCount - 1, target + 1);
|
|
6277
|
+
const focused = this.#body.contains(document.activeElement)
|
|
6278
|
+
? /** @type Element */ (document.activeElement).closest('tr')
|
|
6279
|
+
: null;
|
|
6280
|
+
const focusedPage = [...window.pages].find(([, rows]) => focused && rows.includes(focused))?.[0];
|
|
6281
|
+
if (focusedPage !== undefined && Math.max(last, focusedPage) - Math.min(first, focusedPage) <= 4) {
|
|
6282
|
+
first = Math.min(first, focusedPage);
|
|
6283
|
+
last = Math.max(last, focusedPage);
|
|
6284
|
+
}
|
|
6285
|
+
if (first === window.first && last === window.last) {
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
clearTimeout(this.#jump);
|
|
6289
|
+
if (!settled && (last < window.first - 1 || first > window.last + 1)) {
|
|
6290
|
+
this.#jump = setTimeout(() => this.#reconcile(true), 150);
|
|
6291
|
+
return;
|
|
6292
|
+
}
|
|
6293
|
+
this.#shiftWindow(window, first, last);
|
|
6294
|
+
}
|
|
6295
|
+
/**
|
|
6296
|
+
* Asks for the pages of the new window that are not in the page, drops
|
|
6297
|
+
* the ones outside it and keeps the row in view where it is.
|
|
6298
|
+
* @param {{ first: number, last: number, pages: Map<number, Element[]> }} window
|
|
6299
|
+
* @param {number} first
|
|
6300
|
+
* @param {number} last
|
|
6301
|
+
*/
|
|
6302
|
+
async #shiftWindow(window, first, last) {
|
|
6303
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
6304
|
+
const claim = this.#loads.hold();
|
|
6305
|
+
this.#shifting = claim;
|
|
6306
|
+
const { sortRequest, filterRequest } = this.#latestRequest;
|
|
6307
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
6308
|
+
const wanted = Array.from({ length: last - first + 1 }, (_, i) => first + i);
|
|
6309
|
+
const missing = wanted.filter((page) => !window.pages.has(page));
|
|
6310
|
+
status.failure.setAttribute('hidden', '');
|
|
6311
|
+
status.appending.removeAttribute('hidden');
|
|
6312
|
+
try {
|
|
6313
|
+
const answers = await Promise.all(
|
|
6314
|
+
missing.map(async (page) => {
|
|
6315
|
+
const pageRequest = { page, size };
|
|
6316
|
+
return { pageRequest, response: await this.#loader.load(pageRequest, sortRequest, filterRequest) };
|
|
6317
|
+
}),
|
|
6318
|
+
);
|
|
6319
|
+
if (claim.stale) {
|
|
6320
|
+
return;
|
|
6321
|
+
}
|
|
6322
|
+
const kept = wanted.filter((page) => window.pages.has(page));
|
|
6323
|
+
const top = this.#wrapper.getBoundingClientRect().top;
|
|
6324
|
+
const anchor = kept
|
|
6325
|
+
.flatMap((page) => window.pages.get(page) ?? [])
|
|
6326
|
+
.find((row) => row.getBoundingClientRect().bottom > top);
|
|
6327
|
+
const anchorTop = anchor?.getBoundingClientRect().top ?? 0;
|
|
6328
|
+
for (const [page, rows] of window.pages) {
|
|
6329
|
+
if (page < first || page > last) {
|
|
6330
|
+
for (const row of rows) {
|
|
6331
|
+
row.remove();
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
/** @type {Map<number, Element[]>} */
|
|
6336
|
+
const pages = new Map();
|
|
6337
|
+
for (const page of wanted) {
|
|
6338
|
+
const answer = answers.find((a) => a.pageRequest.page === page);
|
|
6339
|
+
pages.set(
|
|
6340
|
+
page,
|
|
6341
|
+
window.pages.get(page) ??
|
|
6342
|
+
(answer ? this.#pageRows(answer.pageRequest, filterRequest, answer.response) : []),
|
|
6343
|
+
);
|
|
6344
|
+
}
|
|
6345
|
+
if (kept.length === 0) {
|
|
6346
|
+
this.#body.replaceChildren(...[...pages.values()].flat());
|
|
6347
|
+
} else {
|
|
6348
|
+
this.#body.prepend(...wanted.filter((page) => page < kept[0]).flatMap((page) => pages.get(page) ?? []));
|
|
6349
|
+
this.#body.append(
|
|
6350
|
+
...wanted.filter((page) => page > /** @type number */ (kept.at(-1))).flatMap((page) => pages.get(page) ?? []),
|
|
6351
|
+
);
|
|
6352
|
+
}
|
|
6353
|
+
this.#window = { first, last, pages };
|
|
6354
|
+
this.#total = answers.at(-1)?.response.size ?? this.#total;
|
|
6355
|
+
this.#body.parentElement?.setAttribute('aria-rowcount', String(this.#total + 1));
|
|
6356
|
+
this.#fitSpacers();
|
|
6357
|
+
if (anchor) {
|
|
6358
|
+
this.#wrapper.scrollTop += anchor.getBoundingClientRect().top - anchorTop;
|
|
6359
|
+
}
|
|
6360
|
+
await index_mjs.Rendering.waitForChildren(this.#body).catch(() => undefined);
|
|
6361
|
+
if (claim.stale) {
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
if (anchor) {
|
|
6365
|
+
// the elements in the new rows have rendered since, which can change their height
|
|
6366
|
+
this.#wrapper.scrollTop += anchor.getBoundingClientRect().top - anchorTop;
|
|
6367
|
+
}
|
|
6368
|
+
status.appending.setAttribute('hidden', '');
|
|
6369
|
+
this.#countInView();
|
|
6370
|
+
for (const { pageRequest, response } of answers) {
|
|
6371
|
+
this.dispatchEvent(
|
|
6372
|
+
new CustomEvent('load:success', {
|
|
6373
|
+
bubbles: true,
|
|
6374
|
+
cancelable: false,
|
|
6375
|
+
detail: { pageRequest, sortRequest, filterRequest, response },
|
|
6376
|
+
}),
|
|
6377
|
+
);
|
|
6378
|
+
}
|
|
6379
|
+
} catch (/** @type any */ error) {
|
|
6380
|
+
if (claim.stale) {
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
this.#windowFailed = true;
|
|
6384
|
+
status.appending.setAttribute('hidden', '');
|
|
6385
|
+
status.error.textContent = index_mjs.Localization.of().failure(error);
|
|
6386
|
+
status.failure.removeAttribute('hidden');
|
|
6387
|
+
this.dispatchEvent(
|
|
6388
|
+
new CustomEvent('load:failure', {
|
|
6389
|
+
bubbles: true,
|
|
6390
|
+
cancelable: false,
|
|
6391
|
+
detail: { pageRequest: { page: missing[0], size }, sortRequest, filterRequest, exception: error },
|
|
6392
|
+
}),
|
|
6393
|
+
);
|
|
6394
|
+
return;
|
|
6395
|
+
} finally {
|
|
6396
|
+
if (this.#shifting === claim) {
|
|
6397
|
+
this.#shifting = null;
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
this.#reconcile(false);
|
|
6401
|
+
}
|
|
6402
|
+
/**
|
|
6403
|
+
* Asks again for the pages of the window and shows them where they are.
|
|
6404
|
+
* @returns {Promise<void>}
|
|
6405
|
+
*/
|
|
6406
|
+
async #reloadWindow() {
|
|
6407
|
+
const window = /** @type {{ first: number, last: number, pages: Map<number, Element[]> }} */ (this.#window);
|
|
6408
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
6409
|
+
const { sortRequest, filterRequest } = this.#latestRequest;
|
|
6410
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
6411
|
+
this.#requestedFilters = filterRequest;
|
|
6412
|
+
const claim = this.#loads.take();
|
|
6413
|
+
clearTimeout(this.#jump);
|
|
6414
|
+
this.#shifting = null;
|
|
6415
|
+
this.#windowFailed = false;
|
|
6416
|
+
status.appending.setAttribute('hidden', '');
|
|
6417
|
+
status.failure.setAttribute('hidden', '');
|
|
6418
|
+
this.setAttribute('aria-busy', 'true');
|
|
6419
|
+
const wanted = Array.from({ length: window.last - window.first + 1 }, (_, i) => window.first + i);
|
|
6420
|
+
try {
|
|
6421
|
+
const answers = await Promise.all(
|
|
6422
|
+
wanted.map(async (page) => {
|
|
6423
|
+
const pageRequest = { page, size };
|
|
6424
|
+
return { pageRequest, response: await this.#loader.load(pageRequest, sortRequest, filterRequest) };
|
|
6425
|
+
}),
|
|
6426
|
+
);
|
|
6427
|
+
if (claim.stale) {
|
|
6428
|
+
return;
|
|
6429
|
+
}
|
|
6430
|
+
const total = /** @type {{ response: TablePageResponse }} */ (answers.at(-1)).response.size;
|
|
6431
|
+
const pageCount = Math.ceil(total / size);
|
|
6432
|
+
if (!(window.first < pageCount)) {
|
|
6433
|
+
return await this.load({ page: 0, size }, sortRequest, filterRequest);
|
|
6434
|
+
}
|
|
6435
|
+
const shown = answers.filter((a) => a.pageRequest.page < pageCount);
|
|
6436
|
+
const pages = new Map(
|
|
6437
|
+
shown.map((a) => [a.pageRequest.page, this.#pageRows(a.pageRequest, filterRequest, a.response)]),
|
|
6438
|
+
);
|
|
6439
|
+
this.#body.replaceChildren(...[...pages.values()].flat());
|
|
6440
|
+
this.#window = { first: window.first, last: Math.min(window.last, pageCount - 1), pages };
|
|
6441
|
+
this.#total = total;
|
|
6442
|
+
this.#body.parentElement?.setAttribute('aria-rowcount', String(total + 1));
|
|
6443
|
+
this.#fitSpacers();
|
|
6444
|
+
await index_mjs.Rendering.waitForChildren(this.#body).catch(() => undefined);
|
|
6445
|
+
if (claim.stale) {
|
|
6446
|
+
return;
|
|
6447
|
+
}
|
|
6448
|
+
this.removeAttribute('aria-busy');
|
|
6449
|
+
this.#countInView();
|
|
6450
|
+
for (const { pageRequest, response } of shown) {
|
|
6451
|
+
this.dispatchEvent(
|
|
6452
|
+
new CustomEvent('load:success', {
|
|
6453
|
+
bubbles: true,
|
|
6454
|
+
cancelable: false,
|
|
6455
|
+
detail: { pageRequest, sortRequest, filterRequest, response },
|
|
6456
|
+
}),
|
|
6457
|
+
);
|
|
6458
|
+
}
|
|
6459
|
+
this.#reconcile(false);
|
|
6460
|
+
} catch (/** @type any */ error) {
|
|
6461
|
+
if (claim.stale) {
|
|
6462
|
+
return;
|
|
6463
|
+
}
|
|
6464
|
+
this.#fail(error, { pageRequest: { page: window.first, size }, sortRequest, filterRequest });
|
|
6465
|
+
throw error;
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
#update(pageRequest, sortRequest, filterRequest, pageResponse) {
|
|
6469
|
+
this.#loading.setAttribute('hidden', '');
|
|
6470
|
+
if (this.#virtual) {
|
|
6471
|
+
this.#rowHeight = 0;
|
|
6472
|
+
this.#unlockColumns();
|
|
6473
|
+
}
|
|
6474
|
+
const rows = this.#pageRows(pageRequest, filterRequest, pageResponse);
|
|
6475
|
+
this.#body.replaceChildren(...rows);
|
|
5808
6476
|
this.#empty.toggleAttribute('hidden', pageResponse.data.length !== 0);
|
|
5809
6477
|
if (this.#emptyTemplate && pageResponse.data.length === 0) {
|
|
5810
6478
|
this.#emptyAuthors.replaceChildren(
|
|
@@ -5813,7 +6481,28 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5813
6481
|
.render(),
|
|
5814
6482
|
);
|
|
5815
6483
|
}
|
|
5816
|
-
this.#
|
|
6484
|
+
if (this.#virtual) {
|
|
6485
|
+
this.#wrapper.scrollTop = 0;
|
|
6486
|
+
this.#total = pageResponse.size;
|
|
6487
|
+
this.#window =
|
|
6488
|
+
rows.length > 0 ? { first: pageRequest.page, last: pageRequest.page, pages: new Map([[pageRequest.page, rows]]) } : null;
|
|
6489
|
+
return;
|
|
6490
|
+
}
|
|
6491
|
+
if (this.#status) {
|
|
6492
|
+
this.#wrapper.scrollTop = 0;
|
|
6493
|
+
this.#appended = {
|
|
6494
|
+
next: pageRequest.page + 1,
|
|
6495
|
+
shown: pageResponse.data.length,
|
|
6496
|
+
total: pageResponse.size,
|
|
6497
|
+
last: pageResponse.data.length,
|
|
6498
|
+
};
|
|
6499
|
+
this.#status.count.textContent = index_mjs.Localization.of().t('table.loaded', {
|
|
6500
|
+
loaded: this.#appended.shown,
|
|
6501
|
+
total: this.#appended.total,
|
|
6502
|
+
});
|
|
6503
|
+
return;
|
|
6504
|
+
}
|
|
6505
|
+
this.#paginator.update({ current: pageRequest.page, total: Math.ceil(pageResponse.size / pageRequest.size) });
|
|
5817
6506
|
}
|
|
5818
6507
|
}
|
|
5819
6508
|
|
|
@@ -6989,8 +7678,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6989
7678
|
* failure's problems, one per line, or its message when it carries none.
|
|
6990
7679
|
*/
|
|
6991
7680
|
class SectionRequests {
|
|
6992
|
-
/** @type {WeakSet<Element>} */
|
|
7681
|
+
/** @type {WeakSet<Element>} the sections a first delivery succeeded for */
|
|
6993
7682
|
#entered = new WeakSet();
|
|
7683
|
+
/** @type {WeakSet<Element>} the sections whose first delivery is in flight */
|
|
7684
|
+
#entering = new WeakSet();
|
|
6994
7685
|
/** @type {WeakMap<Element, Claims>} */
|
|
6995
7686
|
#claims = new WeakMap();
|
|
6996
7687
|
|
|
@@ -6999,9 +7690,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6999
7690
|
* is given, then `section:requested:<name>` when a name is given, each
|
|
7000
7691
|
* bubbling from `host` with the same detail `{ name, section, index, first }`,
|
|
7001
7692
|
* and awaits every answer attached to any of them. `first` is true until a
|
|
7002
|
-
*
|
|
7003
|
-
*
|
|
7004
|
-
*
|
|
7693
|
+
* delivery of the section succeeds: a request nobody listened to does not
|
|
7694
|
+
* spend it, and neither does one whose answers failed, so the next request
|
|
7695
|
+
* is a first one again. While a first delivery is in flight a newer request
|
|
7696
|
+
* for the same section carries `first` false, so the content is not
|
|
7697
|
+
* delivered twice. A request with answers removes the error a previous
|
|
7698
|
+
* failure painted before it waits.
|
|
7005
7699
|
* @param {Element} host the component the events are dispatched on
|
|
7006
7700
|
* @param {Element} section the element the answers deliver into
|
|
7007
7701
|
* @param {string|null} name the section's `data-step`, when it has one
|
|
@@ -7012,7 +7706,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7012
7706
|
* problems in the section unless a newer request superseded this one
|
|
7013
7707
|
*/
|
|
7014
7708
|
async request(host, section, name, index) {
|
|
7015
|
-
const first = !this.#entered.has(section);
|
|
7709
|
+
const first = !this.#entered.has(section) && !this.#entering.has(section);
|
|
7016
7710
|
const detail = { name, section, index, first };
|
|
7017
7711
|
const types = [
|
|
7018
7712
|
'section:requested',
|
|
@@ -7030,7 +7724,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7030
7724
|
if (promises.length === 0) {
|
|
7031
7725
|
return undefined;
|
|
7032
7726
|
}
|
|
7033
|
-
|
|
7727
|
+
if (first) {
|
|
7728
|
+
this.#entering.add(section);
|
|
7729
|
+
}
|
|
7034
7730
|
let claims = this.#claims.get(section);
|
|
7035
7731
|
if (!claims) {
|
|
7036
7732
|
claims = new Claims();
|
|
@@ -7046,13 +7742,20 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7046
7742
|
}
|
|
7047
7743
|
});
|
|
7048
7744
|
try {
|
|
7049
|
-
|
|
7745
|
+
const answers = await Promise.all(promises);
|
|
7746
|
+
if (first) {
|
|
7747
|
+
this.#entered.add(section);
|
|
7748
|
+
}
|
|
7749
|
+
return answers;
|
|
7050
7750
|
} catch (cause) {
|
|
7051
7751
|
if (owned()) {
|
|
7052
7752
|
this.#paintError(section, cause);
|
|
7053
7753
|
}
|
|
7054
7754
|
throw cause;
|
|
7055
7755
|
} finally {
|
|
7756
|
+
if (first) {
|
|
7757
|
+
this.#entering.delete(section);
|
|
7758
|
+
}
|
|
7056
7759
|
cancelAnimationFrame(frame);
|
|
7057
7760
|
if (owned()) {
|
|
7058
7761
|
section.toggleAttribute('loading', false);
|
|
@@ -7169,15 +7872,27 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7169
7872
|
/**
|
|
7170
7873
|
* Calls `dismiss` when a press and its release both land on the backdrop,
|
|
7171
7874
|
* so a selection dragged out of the panel and released outside does not.
|
|
7875
|
+
* The backdrop is where the pointer is outside the dialog's rectangle: a
|
|
7876
|
+
* part of the panel no child covers, the room around the error section
|
|
7877
|
+
* for one, is the panel.
|
|
7172
7878
|
* @param {() => void} dismiss
|
|
7173
7879
|
*/
|
|
7174
7880
|
onBackdrop(dismiss) {
|
|
7881
|
+
const onBackdrop = (/** @type MouseEvent */ e) => {
|
|
7882
|
+
if (e.target !== this.#dialog) {
|
|
7883
|
+
return false;
|
|
7884
|
+
}
|
|
7885
|
+
const panel = this.#dialog.getBoundingClientRect();
|
|
7886
|
+
return (
|
|
7887
|
+
e.clientX < panel.left || e.clientX > panel.right || e.clientY < panel.top || e.clientY > panel.bottom
|
|
7888
|
+
);
|
|
7889
|
+
};
|
|
7175
7890
|
let pressedOutside = false;
|
|
7176
7891
|
this.#dialog.addEventListener('mousedown', (e) => {
|
|
7177
|
-
pressedOutside = e
|
|
7892
|
+
pressedOutside = onBackdrop(e);
|
|
7178
7893
|
});
|
|
7179
7894
|
this.#dialog.addEventListener('click', (e) => {
|
|
7180
|
-
if (pressedOutside && e
|
|
7895
|
+
if (pressedOutside && onBackdrop(e)) {
|
|
7181
7896
|
dismiss();
|
|
7182
7897
|
}
|
|
7183
7898
|
});
|
|
@@ -7454,7 +8169,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7454
8169
|
* opening, hides the error and loading sections, and dispatches
|
|
7455
8170
|
* `section:requested` on the element (bubbling, with
|
|
7456
8171
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
7457
|
-
* the body and `first` true
|
|
8172
|
+
* the body and `first` true until a delivery succeeds), so a
|
|
7458
8173
|
* listener can fill the body. A failed request is painted into the body
|
|
7459
8174
|
* and does not reject. On a dialog already open nothing is shown or
|
|
7460
8175
|
* requested, and the call waits for the same ending as the calls before it;
|
|
@@ -7706,11 +8421,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7706
8421
|
#answer = null;
|
|
7707
8422
|
#resolvers = [];
|
|
7708
8423
|
#closing = false;
|
|
7709
|
-
#
|
|
7710
|
-
if (e.target === this.#dialog) {
|
|
7711
|
-
this.#closeNow();
|
|
7712
|
-
}
|
|
7713
|
-
};
|
|
8424
|
+
#slides = new Claims();
|
|
7714
8425
|
/**
|
|
7715
8426
|
* @param {{ slots: Record<string, DocumentFragment> }} c
|
|
7716
8427
|
*/
|
|
@@ -7837,7 +8548,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7837
8548
|
* opening, hides the error and loading sections, and dispatches
|
|
7838
8549
|
* `section:requested` on the element (bubbling, with
|
|
7839
8550
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
7840
|
-
* the content section and `first` true
|
|
8551
|
+
* the content section and `first` true until a delivery succeeds),
|
|
7841
8552
|
* so a listener can fill the content. A failed request is painted into the
|
|
7842
8553
|
* content section and does not reject. On a drawer already open nothing is
|
|
7843
8554
|
* shown or requested, except that one sliding out stops and stays open,
|
|
@@ -7888,11 +8599,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7888
8599
|
/**
|
|
7889
8600
|
* Slides the drawer out and closes it when the animation ends, setting the
|
|
7890
8601
|
* `closing` attribute on the native dialog meanwhile; the `close` event is
|
|
7891
|
-
* dispatched then, after this call has returned.
|
|
7892
|
-
*
|
|
7893
|
-
*
|
|
7894
|
-
*
|
|
7895
|
-
*
|
|
8602
|
+
* dispatched then, after this call has returned. An animation the page
|
|
8603
|
+
* cancels, by hiding the drawer or an ancestor of it, closes it as one
|
|
8604
|
+
* that ended does. Where the user prefers reduced motion, or no animation
|
|
8605
|
+
* runs, it closes at once. A non-empty result answers as the button
|
|
8606
|
+
* carrying that `data-result` would; no result, or an empty one, is a
|
|
8607
|
+
* dismissal. Does nothing on a drawer that is closed or already sliding
|
|
8608
|
+
* out.
|
|
7896
8609
|
* @param {string} [result]
|
|
7897
8610
|
*/
|
|
7898
8611
|
close(result) {
|
|
@@ -7907,12 +8620,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7907
8620
|
return;
|
|
7908
8621
|
}
|
|
7909
8622
|
this.#dialog.setAttribute('closing', '');
|
|
7910
|
-
|
|
8623
|
+
const animations = this.#dialog.getAnimations();
|
|
8624
|
+
if (animations.length === 0) {
|
|
7911
8625
|
this.#closeNow();
|
|
7912
8626
|
return;
|
|
7913
8627
|
}
|
|
7914
8628
|
this.#closing = true;
|
|
7915
|
-
this.#
|
|
8629
|
+
const slide = this.#slides.take();
|
|
8630
|
+
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
|
8631
|
+
if (!slide.stale) {
|
|
8632
|
+
this.#closeNow();
|
|
8633
|
+
}
|
|
8634
|
+
});
|
|
7916
8635
|
}
|
|
7917
8636
|
#closeNow() {
|
|
7918
8637
|
this.#stopSlidingOut();
|
|
@@ -7920,7 +8639,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7920
8639
|
}
|
|
7921
8640
|
#stopSlidingOut() {
|
|
7922
8641
|
this.#closing = false;
|
|
7923
|
-
this.#
|
|
8642
|
+
this.#slides.invalidate();
|
|
7924
8643
|
this.#dialog.removeAttribute('closing');
|
|
7925
8644
|
}
|
|
7926
8645
|
/** @returns {boolean} whether this call is the one that opened the modal */
|
|
@@ -8465,7 +9184,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8465
9184
|
}
|
|
8466
9185
|
/**
|
|
8467
9186
|
* Fires the section requests again for a panel, whether it is visible or
|
|
8468
|
-
* not, with `first` false once an earlier
|
|
9187
|
+
* not, with `first` false once an earlier delivery succeeded.
|
|
8469
9188
|
* @param {number | Element} ref the panel's index, or the panel itself
|
|
8470
9189
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
8471
9190
|
* the answers, or to undefined when nobody listened or the delivery
|
|
@@ -8688,7 +9407,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8688
9407
|
}
|
|
8689
9408
|
/**
|
|
8690
9409
|
* Fires the section requests again for a section, whether it is current or
|
|
8691
|
-
* not, with `first` false once an earlier
|
|
9410
|
+
* not, with `first` false once an earlier delivery succeeded.
|
|
8692
9411
|
* @param {string | Element} ref the section's `data-step`, or the section
|
|
8693
9412
|
* itself
|
|
8694
9413
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
@@ -8750,6 +9469,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8750
9469
|
'table.initial': 'Start searching to see results.',
|
|
8751
9470
|
'table.error': 'Error while loading data:',
|
|
8752
9471
|
'table.no-data': 'No elements found.',
|
|
9472
|
+
'table.loaded': 'Showing {loaded} of {total}',
|
|
9473
|
+
'table.range': 'Rows {from}-{to} of {total}',
|
|
9474
|
+
'table.retry': 'Retry',
|
|
8753
9475
|
'dropdown.empty': 'No results',
|
|
8754
9476
|
'select.remove': 'Remove',
|
|
8755
9477
|
'files.dropzone-label': 'Click or drop your files here',
|
|
@@ -8801,6 +9523,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8801
9523
|
'table.initial': 'Avvia la ricerca per visualizzare i risultati.',
|
|
8802
9524
|
'table.error': 'Errore nel caricamento dei dati:',
|
|
8803
9525
|
'table.no-data': 'Nessun elemento trovato.',
|
|
9526
|
+
'table.loaded': 'Mostrati {loaded} di {total}',
|
|
9527
|
+
'table.range': 'Righe {from}-{to} di {total}',
|
|
9528
|
+
'table.retry': 'Riprova',
|
|
8804
9529
|
'dropdown.empty': 'Nessun risultato',
|
|
8805
9530
|
'select.remove': 'Rimuovi',
|
|
8806
9531
|
'files.dropzone-label': 'Clicca o trascina i file qui',
|
|
@@ -8852,6 +9577,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8852
9577
|
'table.initial': 'Inicia la búsqueda para ver los resultados.',
|
|
8853
9578
|
'table.error': 'Error al cargar los datos:',
|
|
8854
9579
|
'table.no-data': 'No se encontraron elementos.',
|
|
9580
|
+
'table.loaded': 'Mostrando {loaded} de {total}',
|
|
9581
|
+
'table.range': 'Filas {from}-{to} de {total}',
|
|
9582
|
+
'table.retry': 'Reintentar',
|
|
8855
9583
|
'dropdown.empty': 'Sin resultados',
|
|
8856
9584
|
'select.remove': 'Eliminar',
|
|
8857
9585
|
'files.dropzone-label': 'Haz clic o arrastra tus archivos aquí',
|
|
@@ -8906,6 +9634,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8906
9634
|
'table.initial': 'Lancez la recherche pour voir les résultats.',
|
|
8907
9635
|
'table.error': 'Erreur lors du chargement des données :',
|
|
8908
9636
|
'table.no-data': 'Aucun élément trouvé.',
|
|
9637
|
+
'table.loaded': 'Affichage de {loaded} sur {total}',
|
|
9638
|
+
'table.range': 'Lignes {from} à {to} sur {total}',
|
|
9639
|
+
'table.retry': 'Réessayer',
|
|
8909
9640
|
'dropdown.empty': 'Aucun résultat',
|
|
8910
9641
|
'select.remove': 'Retirer',
|
|
8911
9642
|
'files.dropzone-label': 'Cliquez ou déposez vos fichiers ici',
|
|
@@ -8967,8 +9698,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8967
9698
|
/**
|
|
8968
9699
|
* @param {{ language?: string, translations?: Record<string, any>, httpClient?: HttpClient }} [options]
|
|
8969
9700
|
* `language` is fixed for the page: a full BCP-47 tag or a primary subtag,
|
|
8970
|
-
* defaulting to `navigator.language`, then to 'en'
|
|
8971
|
-
*
|
|
9701
|
+
* defaulting to `navigator.language`, then to 'en', an empty one included.
|
|
9702
|
+
* It is canonicalized (`IT-it` and `it_IT` both read `it-IT`), and one that
|
|
9703
|
+
* is no language tag at all throws here, rather than at the first message
|
|
9704
|
+
* or number formatted in it. Its primary subtag picks the built-in
|
|
9705
|
+
* translations (en, it, es, fr); the messages
|
|
8972
9706
|
* are the english ones, overridden by that language's, overridden by
|
|
8973
9707
|
* `translations`, so a language without built-in translations reads in
|
|
8974
9708
|
* english. `translations` is a flat map of keys to messages: reword built-in
|
|
@@ -8976,9 +9710,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8976
9710
|
* `httpClient` is the client every ful component fetches through, registered
|
|
8977
9711
|
* as the `http-client` component; without one, `configure` builds a client
|
|
8978
9712
|
* that sends the csrf token and navigates to '/' on a 401 response.
|
|
9713
|
+
* @throws {RangeError} when `language` is not a language tag
|
|
8979
9714
|
*/
|
|
8980
9715
|
constructor(options = {}) {
|
|
8981
|
-
|
|
9716
|
+
const language = options.language || navigator?.language || 'en';
|
|
9717
|
+
try {
|
|
9718
|
+
// unicode locale identifiers separate subtags with an underscore too, Intl takes the dash alone
|
|
9719
|
+
this.#language = Intl.getCanonicalLocales(String(language).replaceAll('_', '-'))[0];
|
|
9720
|
+
} catch {
|
|
9721
|
+
throw new RangeError(
|
|
9722
|
+
`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`,
|
|
9723
|
+
);
|
|
9724
|
+
}
|
|
8982
9725
|
this.#translations = options.translations ?? {};
|
|
8983
9726
|
this.#httpClient = options.httpClient ?? null;
|
|
8984
9727
|
}
|
|
@@ -8987,7 +9730,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8987
9730
|
* Defines on `registry` the `l10n` module, the `http-client` component,
|
|
8988
9731
|
* every `ful-*` element, the `loaders:select`, `loaders:form` and
|
|
8989
9732
|
* `loaders:table` components, and an overlay publishing `l10n` (the merged
|
|
8990
|
-
* messages) and `locale` (the `language
|
|
9733
|
+
* messages) and `locale` (the `language`, canonicalized) to every template.
|
|
8991
9734
|
* @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
9735
|
* the ftl `Registry`, each of those methods answering the registry itself
|
|
8993
9736
|
* @returns {void}
|
|
@@ -8995,7 +9738,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8995
9738
|
configure(registry) {
|
|
8996
9739
|
const httpClient =
|
|
8997
9740
|
this.#httpClient ?? index_mjs$1.HttpClient.builder().withCsrfToken().withRedirectOnUnauthorized('/').build();
|
|
8998
|
-
const language = this.#language.split('-')[0]
|
|
9741
|
+
const language = this.#language.split('-')[0];
|
|
8999
9742
|
const l10n = { ...BUILTIN.en, ...BUILTIN[language], ...this.#translations };
|
|
9000
9743
|
registry
|
|
9001
9744
|
.defineModule('l10n', index_mjs.Localization)
|
|
@@ -9076,7 +9819,6 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
9076
9819
|
exports.SortButton = SortButton;
|
|
9077
9820
|
exports.Table = Table;
|
|
9078
9821
|
exports.TableLoader = TableLoader;
|
|
9079
|
-
exports.TableSchemaParser = TableSchemaParser;
|
|
9080
9822
|
exports.Tabs = Tabs;
|
|
9081
9823
|
exports.TextFilter = TextFilter;
|
|
9082
9824
|
exports.Timing = Timing;
|