@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/fml.iife.js
CHANGED
|
@@ -2850,6 +2850,8 @@ var fml = (function (exports) {
|
|
|
2850
2850
|
return undefined;
|
|
2851
2851
|
};
|
|
2852
2852
|
|
|
2853
|
+
const BOUND = Symbol('BOUND');
|
|
2854
|
+
|
|
2853
2855
|
/**
|
|
2854
2856
|
* Evaluates a parsed expression against a scope. One instance per evaluation:
|
|
2855
2857
|
* it holds the modules and the data stack that names resolve against, and keeps
|
|
@@ -2858,9 +2860,11 @@ var fml = (function (exports) {
|
|
|
2858
2860
|
class EvaluatingVisitor {
|
|
2859
2861
|
#modules;
|
|
2860
2862
|
#dataStack;
|
|
2861
|
-
|
|
2863
|
+
#bound;
|
|
2864
|
+
constructor(modules, dataStack, bound) {
|
|
2862
2865
|
this.#modules = modules;
|
|
2863
2866
|
this.#dataStack = dataStack;
|
|
2867
|
+
this.#bound = bound ?? dataStack.length;
|
|
2864
2868
|
}
|
|
2865
2869
|
#resolve(prop) {
|
|
2866
2870
|
return resolveInStack(this.#dataStack, prop);
|
|
@@ -2884,8 +2888,9 @@ var fml = (function (exports) {
|
|
|
2884
2888
|
#cached_resolve_proxy;
|
|
2885
2889
|
#resolve_proxy() {
|
|
2886
2890
|
if (!this.#cached_resolve_proxy) {
|
|
2891
|
+
const bound = (name) => resolveInStack(this.#dataStack.slice(0, this.#bound), name);
|
|
2887
2892
|
this.#cached_resolve_proxy = new Proxy(this.#dataStack, {
|
|
2888
|
-
get: (target, prop) => this.#resolve(prop),
|
|
2893
|
+
get: (target, prop) => (prop === BOUND ? bound : this.#resolve(prop)),
|
|
2889
2894
|
});
|
|
2890
2895
|
}
|
|
2891
2896
|
return this.#cached_resolve_proxy;
|
|
@@ -3038,6 +3043,16 @@ var fml = (function (exports) {
|
|
|
3038
3043
|
* `{{{ }}}` (html) or `{{{{ }}}}` (node) interpolation, in order.
|
|
3039
3044
|
*/
|
|
3040
3045
|
static MODE_TEMPLATED = Symbol('MODE_TEMPLATED');
|
|
3046
|
+
/**
|
|
3047
|
+
* The key under which the receiver of a module function answers a lookup
|
|
3048
|
+
* against the bound data alone: `this[Expressions.BOUND](name)` resolves
|
|
3049
|
+
* the name in the layers the scope was created with (a registry's data,
|
|
3050
|
+
* the data handed to a Template factory) and never in an overlay pushed
|
|
3051
|
+
* afterwards, by `data-tpl-each`, `data-tpl-with` or `withOverlay`. It is
|
|
3052
|
+
* how a module reads what the page configured without the rendered data
|
|
3053
|
+
* being able to supply or replace it. No name on the data stack reaches it.
|
|
3054
|
+
*/
|
|
3055
|
+
static BOUND = BOUND;
|
|
3041
3056
|
|
|
3042
3057
|
static #astCache = new BoundedCache(1000);
|
|
3043
3058
|
|
|
@@ -3062,10 +3077,11 @@ var fml = (function (exports) {
|
|
|
3062
3077
|
* @param {any[]} dataStack innermost overlay last
|
|
3063
3078
|
* @param {any} ast what `parse` answered
|
|
3064
3079
|
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] the mode it was parsed in
|
|
3080
|
+
* @param {number} [bound] how many layers, from the outermost, are the bound data; all of them when omitted
|
|
3065
3081
|
* @returns {any} the value, or the list of parts in MODE_TEMPLATED
|
|
3066
3082
|
*/
|
|
3067
|
-
static evaluate(modules, dataStack, ast, mode) {
|
|
3068
|
-
return new EvaluatingVisitor(modules, dataStack).visitRoot(ast, mode === Expressions.MODE_TEMPLATED);
|
|
3083
|
+
static evaluate(modules, dataStack, ast, mode, bound) {
|
|
3084
|
+
return new EvaluatingVisitor(modules, dataStack, bound).visitRoot(ast, mode === Expressions.MODE_TEMPLATED);
|
|
3069
3085
|
}
|
|
3070
3086
|
/**
|
|
3071
3087
|
* Parses and evaluates an expression.
|
|
@@ -3073,10 +3089,11 @@ var fml = (function (exports) {
|
|
|
3073
3089
|
* @param {any[]} dataStack innermost overlay last
|
|
3074
3090
|
* @param {string} expression
|
|
3075
3091
|
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] MODE_EXPRESSION when omitted
|
|
3092
|
+
* @param {number} [bound] how many layers, from the outermost, are the bound data; all of them when omitted
|
|
3076
3093
|
* @returns {any} the value, or the list of parts in MODE_TEMPLATED
|
|
3077
3094
|
*/
|
|
3078
|
-
static interpret(modules, dataStack, expression, mode) {
|
|
3079
|
-
return Expressions.evaluate(modules, dataStack, Expressions.parse(expression, mode), mode);
|
|
3095
|
+
static interpret(modules, dataStack, expression, mode, bound) {
|
|
3096
|
+
return Expressions.evaluate(modules, dataStack, Expressions.parse(expression, mode), mode, bound);
|
|
3080
3097
|
}
|
|
3081
3098
|
}
|
|
3082
3099
|
|
|
@@ -3085,17 +3102,24 @@ var fml = (function (exports) {
|
|
|
3085
3102
|
* that a bare identifier resolves against, as a single value. Adding an overlay
|
|
3086
3103
|
* returns a new evaluator instead of modifying this one, so a template can add
|
|
3087
3104
|
* data for one subtree without affecting the rest of the render.
|
|
3105
|
+
*
|
|
3106
|
+
* The layers an evaluator is created with are its bound data; the overlays
|
|
3107
|
+
* pushed afterwards are not. A bare identifier resolves against both, while a
|
|
3108
|
+
* module function can ask for the bound data alone through `Expressions.BOUND`.
|
|
3088
3109
|
*/
|
|
3089
3110
|
class ExpressionEvaluator {
|
|
3090
3111
|
#modules;
|
|
3091
3112
|
#dataStack;
|
|
3113
|
+
#bound;
|
|
3092
3114
|
/**
|
|
3093
3115
|
* @param {{ [k: string]: any } | null | undefined} modules
|
|
3094
3116
|
* @param {any[]} dataStack innermost overlay last
|
|
3117
|
+
* @param {number} [bound] how many layers, from the outermost, are the bound data; all of them when omitted
|
|
3095
3118
|
*/
|
|
3096
|
-
constructor(modules, dataStack) {
|
|
3119
|
+
constructor(modules, dataStack, bound) {
|
|
3097
3120
|
this.#modules = modules;
|
|
3098
3121
|
this.#dataStack = dataStack;
|
|
3122
|
+
this.#bound = bound ?? dataStack.length;
|
|
3099
3123
|
}
|
|
3100
3124
|
/**
|
|
3101
3125
|
* A new evaluator with one more module: `#name:fn()` resolves into it, and an
|
|
@@ -3106,11 +3130,11 @@ var fml = (function (exports) {
|
|
|
3106
3130
|
*/
|
|
3107
3131
|
withModule(name, value) {
|
|
3108
3132
|
const module = name ? { [name]: value } : value;
|
|
3109
|
-
return new ExpressionEvaluator({ ...this.#modules, ...module }, this.#dataStack);
|
|
3133
|
+
return new ExpressionEvaluator({ ...this.#modules, ...module }, this.#dataStack, this.#bound);
|
|
3110
3134
|
}
|
|
3111
3135
|
/**
|
|
3112
3136
|
* A new evaluator with the data pushed as the innermost overlays, the last
|
|
3113
|
-
* one winning a shared name.
|
|
3137
|
+
* one winning a shared name. The bound data stays what it was.
|
|
3114
3138
|
* @param {...any} data
|
|
3115
3139
|
* @returns {ExpressionEvaluator}
|
|
3116
3140
|
*/
|
|
@@ -3118,6 +3142,7 @@ var fml = (function (exports) {
|
|
|
3118
3142
|
return new ExpressionEvaluator(
|
|
3119
3143
|
this.#modules,
|
|
3120
3144
|
data.length === 0 ? this.#dataStack : [...this.#dataStack, ...data],
|
|
3145
|
+
this.#bound,
|
|
3121
3146
|
);
|
|
3122
3147
|
}
|
|
3123
3148
|
/**
|
|
@@ -3135,7 +3160,13 @@ var fml = (function (exports) {
|
|
|
3135
3160
|
* @returns {any}
|
|
3136
3161
|
*/
|
|
3137
3162
|
evaluateExpression(expression) {
|
|
3138
|
-
return Expressions.interpret(
|
|
3163
|
+
return Expressions.interpret(
|
|
3164
|
+
this.#modules,
|
|
3165
|
+
this.#dataStack,
|
|
3166
|
+
expression,
|
|
3167
|
+
Expressions.MODE_EXPRESSION,
|
|
3168
|
+
this.#bound,
|
|
3169
|
+
);
|
|
3139
3170
|
}
|
|
3140
3171
|
/**
|
|
3141
3172
|
* Evaluates a templated text, the `{{ }}` form, against this scope.
|
|
@@ -3143,7 +3174,7 @@ var fml = (function (exports) {
|
|
|
3143
3174
|
* @returns {{ type: symbol, value: any }[]} its parts, as MODE_TEMPLATED describes
|
|
3144
3175
|
*/
|
|
3145
3176
|
evaluateTemplated(text) {
|
|
3146
|
-
return Expressions.interpret(this.#modules, this.#dataStack, text, Expressions.MODE_TEMPLATED);
|
|
3177
|
+
return Expressions.interpret(this.#modules, this.#dataStack, text, Expressions.MODE_TEMPLATED, this.#bound);
|
|
3147
3178
|
}
|
|
3148
3179
|
}
|
|
3149
3180
|
|
|
@@ -4039,14 +4070,22 @@ var fml = (function (exports) {
|
|
|
4039
4070
|
#ready = new Promise((resolve) => {
|
|
4040
4071
|
this.#readyResolve = resolve;
|
|
4041
4072
|
});
|
|
4073
|
+
#configured = /** @type {PromiseWithResolvers<void>} */ (Promise.withResolvers());
|
|
4042
4074
|
constructor() {
|
|
4043
|
-
Nodes.waitDomContentLoaded(document).then(() => {
|
|
4075
|
+
Promise.all([Nodes.waitDomContentLoaded(document), this.#configured.promise]).then(() => {
|
|
4044
4076
|
this.#start();
|
|
4045
4077
|
});
|
|
4046
4078
|
}
|
|
4047
4079
|
ready() {
|
|
4048
4080
|
return this.#ready;
|
|
4049
4081
|
}
|
|
4082
|
+
/**
|
|
4083
|
+
* Marks the registry as configured: until then nothing is defined, so an
|
|
4084
|
+
* empty queue says nothing about the page and readiness is held back.
|
|
4085
|
+
*/
|
|
4086
|
+
configured() {
|
|
4087
|
+
this.#configured.resolve();
|
|
4088
|
+
}
|
|
4050
4089
|
async #start() {
|
|
4051
4090
|
await this.settled();
|
|
4052
4091
|
document.dispatchEvent(
|
|
@@ -4089,7 +4128,10 @@ var fml = (function (exports) {
|
|
|
4089
4128
|
* @returns {Promise<void>}
|
|
4090
4129
|
*/
|
|
4091
4130
|
settled() {
|
|
4092
|
-
return this.#drain(
|
|
4131
|
+
return this.#drain(
|
|
4132
|
+
() => true,
|
|
4133
|
+
(entry) => entry.finished,
|
|
4134
|
+
);
|
|
4093
4135
|
}
|
|
4094
4136
|
/**
|
|
4095
4137
|
* Waits for the accepted upgrades, the ones enqueued meanwhile included,
|
|
@@ -4343,7 +4385,9 @@ var fml = (function (exports) {
|
|
|
4343
4385
|
}
|
|
4344
4386
|
/**
|
|
4345
4387
|
* Defines every element deferred so far; from then on, defineElement takes
|
|
4346
|
-
* effect immediately.
|
|
4388
|
+
* effect immediately. Readiness waits for the first call: a page that
|
|
4389
|
+
* configures late (translations fetched first) gets its ftl:ready once the
|
|
4390
|
+
* elements this defines have upgraded, not at DOMContentLoaded.
|
|
4347
4391
|
* @returns {Registry} this registry
|
|
4348
4392
|
*/
|
|
4349
4393
|
configure() {
|
|
@@ -4352,6 +4396,7 @@ var fml = (function (exports) {
|
|
|
4352
4396
|
delete this.#tagToClass[tag];
|
|
4353
4397
|
}
|
|
4354
4398
|
this.#configured = true;
|
|
4399
|
+
this.#upgradeQueue.configured();
|
|
4355
4400
|
return this;
|
|
4356
4401
|
}
|
|
4357
4402
|
/**
|
|
@@ -4383,9 +4428,11 @@ var fml = (function (exports) {
|
|
|
4383
4428
|
/**
|
|
4384
4429
|
* Waits for this registry's readiness: resolves right after its ftl:ready
|
|
4385
4430
|
* event is dispatched on the document, and immediately when that already
|
|
4386
|
-
* happened.
|
|
4387
|
-
*
|
|
4388
|
-
*
|
|
4431
|
+
* happened. The event is dispatched once DOMContentLoaded has passed,
|
|
4432
|
+
* configure() has been called and the queue has drained, so a registry
|
|
4433
|
+
* that is never configured is never ready. Never rejects: a failed
|
|
4434
|
+
* upgrade counts as settled, and its rejection is left unhandled so it
|
|
4435
|
+
* still reaches the console and the page's error reporting.
|
|
4389
4436
|
* @returns {Promise<void>}
|
|
4390
4437
|
*/
|
|
4391
4438
|
ready() {
|
|
@@ -4729,12 +4776,27 @@ var fml = (function (exports) {
|
|
|
4729
4776
|
*/
|
|
4730
4777
|
|
|
4731
4778
|
/**
|
|
4732
|
-
* The receiver contract of the module functions:
|
|
4733
|
-
*
|
|
4779
|
+
* The receiver contract of the module functions: a plain object carrying the
|
|
4780
|
+
* translations and the locale, as Localization.of() binds them, or the proxy
|
|
4781
|
+
* over the data stack a template calls them with.
|
|
4734
4782
|
*
|
|
4735
4783
|
* @typedef {{ l10n?: Messages, locale?: string }} Receiver
|
|
4736
4784
|
*/
|
|
4737
4785
|
|
|
4786
|
+
/**
|
|
4787
|
+
* The translations or the locale of a receiver. From a template they are read
|
|
4788
|
+
* from the bound data alone (the registry's overlays, or the data handed to
|
|
4789
|
+
* the Template factory): the rows and responses a render pushes on top are
|
|
4790
|
+
* what arrives later, and must not supply or replace the page's own messages.
|
|
4791
|
+
* @param {any} receiver
|
|
4792
|
+
* @param {'l10n' | 'locale'} name
|
|
4793
|
+
* @returns {any}
|
|
4794
|
+
*/
|
|
4795
|
+
const configured = (receiver, name) => {
|
|
4796
|
+
const bound = receiver[Expressions.BOUND];
|
|
4797
|
+
return typeof bound === 'function' ? bound(name) : receiver[name];
|
|
4798
|
+
};
|
|
4799
|
+
|
|
4738
4800
|
const PLACEHOLDER = /\{(\w+)\}/g;
|
|
4739
4801
|
const POSITIONAL = /\{(\d+)\}/g;
|
|
4740
4802
|
/** @param {any} v @returns {v is Record<string, string>} */
|
|
@@ -4769,7 +4831,9 @@ var fml = (function (exports) {
|
|
|
4769
4831
|
* The translations as a template module: `#l10n:t()` for messages, plus date,
|
|
4770
4832
|
* number and bytes formatting in the page's locale. The template form and the
|
|
4771
4833
|
* imperative `of()` facade both resolve through the registry's scope, so they
|
|
4772
|
-
* always produce the same result for the same key.
|
|
4834
|
+
* always produce the same result for the same key. A template reads `l10n`
|
|
4835
|
+
* and `locale` from its bound data only, the most recent layer winning: an
|
|
4836
|
+
* overlay pushed while rendering (a row, a response) never reaches them.
|
|
4773
4837
|
*/
|
|
4774
4838
|
class Localization {
|
|
4775
4839
|
/**
|
|
@@ -4788,7 +4852,7 @@ var fml = (function (exports) {
|
|
|
4788
4852
|
* @returns {string} the message, or the key itself when the translations do not carry it
|
|
4789
4853
|
*/
|
|
4790
4854
|
static t(key, ...args) {
|
|
4791
|
-
const messages = this
|
|
4855
|
+
const messages = configured(this, 'l10n') ?? {};
|
|
4792
4856
|
let message = Object.hasOwn(messages, key) ? messages[key] : undefined;
|
|
4793
4857
|
if (message === undefined) {
|
|
4794
4858
|
warnOnce(`missing message "${key}"`);
|
|
@@ -4800,7 +4864,7 @@ var fml = (function (exports) {
|
|
|
4800
4864
|
warnOnce(`plural message "${key}" needs a numeric {count}`);
|
|
4801
4865
|
message = message.other;
|
|
4802
4866
|
} else {
|
|
4803
|
-
const locale = this
|
|
4867
|
+
const locale = configured(this, 'locale') ?? navigator?.language ?? 'en';
|
|
4804
4868
|
const category = formatter(Intl.PluralRules, locale).select(named.count);
|
|
4805
4869
|
message = message[category] ?? message.other;
|
|
4806
4870
|
}
|
|
@@ -4840,7 +4904,7 @@ var fml = (function (exports) {
|
|
|
4840
4904
|
* @returns {string}
|
|
4841
4905
|
*/
|
|
4842
4906
|
static date(value, options) {
|
|
4843
|
-
return formatter(Intl.DateTimeFormat, this
|
|
4907
|
+
return formatter(Intl.DateTimeFormat, configured(this, 'locale'), options).format(value);
|
|
4844
4908
|
}
|
|
4845
4909
|
|
|
4846
4910
|
/**
|
|
@@ -4852,7 +4916,7 @@ var fml = (function (exports) {
|
|
|
4852
4916
|
* @returns {string}
|
|
4853
4917
|
*/
|
|
4854
4918
|
static number(value, options) {
|
|
4855
|
-
return formatter(Intl.NumberFormat, this
|
|
4919
|
+
return formatter(Intl.NumberFormat, configured(this, 'locale'), options).format(value);
|
|
4856
4920
|
}
|
|
4857
4921
|
|
|
4858
4922
|
/**
|
|
@@ -4865,7 +4929,7 @@ var fml = (function (exports) {
|
|
|
4865
4929
|
* @returns {string}
|
|
4866
4930
|
*/
|
|
4867
4931
|
static bytes(value) {
|
|
4868
|
-
const format = formatter(Intl.NumberFormat, this
|
|
4932
|
+
const format = formatter(Intl.NumberFormat, configured(this, 'locale'), { maximumFractionDigits: 2 }).format;
|
|
4869
4933
|
if (value >= 1024 * 1024 * 1024) {
|
|
4870
4934
|
return `${format(value / 1024 / 1024 / 1024)}GiB`;
|
|
4871
4935
|
}
|
|
@@ -6681,12 +6745,13 @@ var fml = (function (exports) {
|
|
|
6681
6745
|
*
|
|
6682
6746
|
* The claim setters, the validity protocol and the aria wiring all act on the
|
|
6683
6747
|
* pieces, so a field with no native control returns a focusable piece of its
|
|
6684
|
-
* own chrome as the control. The claims, `value
|
|
6685
|
-
* members that tolerate a not yet rendered
|
|
6686
|
-
* `value` getter answers what the rendered
|
|
6687
|
-
* declared state,
|
|
6688
|
-
* (see `_holdValue`)
|
|
6689
|
-
* the
|
|
6748
|
+
* own chrome as the control. The claims, `value`, `focus()` and
|
|
6749
|
+
* `setCustomValidity` are the members that tolerate a not yet rendered
|
|
6750
|
+
* element: before the render the `value` getter answers what the rendered
|
|
6751
|
+
* field would answer from its current declared state, a value written then is
|
|
6752
|
+
* held and applied by the render (see `_holdValue`), and a problem reported
|
|
6753
|
+
* then is shown by it. The other members need the pieces and so work only
|
|
6754
|
+
* after the render.
|
|
6690
6755
|
*
|
|
6691
6756
|
* The inner controls carry `form=""`, so the host is the only element the form
|
|
6692
6757
|
* sees. Enter pressed in an inner `<input>` of any type but `file`, `button`,
|
|
@@ -6724,6 +6789,8 @@ var fml = (function (exports) {
|
|
|
6724
6789
|
#also = [];
|
|
6725
6790
|
/** @type {{ value: any } | null} the value written before the render, which the render applies */
|
|
6726
6791
|
#held = null;
|
|
6792
|
+
/** @type {{ error: string, context?: string } | null} the problem reported before the render, which the render shows */
|
|
6793
|
+
#reported = null;
|
|
6727
6794
|
/** Sets the internals' role from the subclass's `static ROLE`. */
|
|
6728
6795
|
constructor() {
|
|
6729
6796
|
super();
|
|
@@ -6788,6 +6855,11 @@ var fml = (function (exports) {
|
|
|
6788
6855
|
this.#pendingFocus = null;
|
|
6789
6856
|
control.focus(options);
|
|
6790
6857
|
}
|
|
6858
|
+
if (this.#reported !== null) {
|
|
6859
|
+
const { error, context } = this.#reported;
|
|
6860
|
+
this.#reported = null;
|
|
6861
|
+
this.setCustomValidity(error, context);
|
|
6862
|
+
}
|
|
6791
6863
|
}
|
|
6792
6864
|
/**
|
|
6793
6865
|
* @param {EventTarget} el
|
|
@@ -6867,11 +6939,17 @@ var fml = (function (exports) {
|
|
|
6867
6939
|
* field owning a whole subtree overrides this to route the message to the
|
|
6868
6940
|
* inner control the path names.
|
|
6869
6941
|
*
|
|
6870
|
-
*
|
|
6942
|
+
* Before the render there is no error region to write to: a message is
|
|
6943
|
+
* kept (the last one wins) and shown once the field has rendered, and a
|
|
6944
|
+
* clear drops the one kept. A field whose render failed shows nothing.
|
|
6871
6945
|
* @param {string} [error] the message, empty or missing to clear
|
|
6872
6946
|
* @param {string} [context] the path below this field's name, '' on an exact match
|
|
6873
6947
|
*/
|
|
6874
6948
|
setCustomValidity(error, context) {
|
|
6949
|
+
if (!this.#fieldError) {
|
|
6950
|
+
this.#reported = error ? { error, context } : null;
|
|
6951
|
+
return;
|
|
6952
|
+
}
|
|
6875
6953
|
Attributes.set(this.#announces ?? this.#control, 'aria-invalid', error ? 'true' : null);
|
|
6876
6954
|
if (!error) {
|
|
6877
6955
|
this.internals.setValidity({});
|
|
@@ -7082,11 +7160,16 @@ var fml = (function (exports) {
|
|
|
7082
7160
|
/**
|
|
7083
7161
|
* Calls `_build(conf)` and wires the pieces it answers. When `_build`
|
|
7084
7162
|
* returns a promise the pieces are wired once it settles; a field that
|
|
7085
|
-
* builds synchronously renders synchronously.
|
|
7163
|
+
* builds synchronously renders synchronously. A default slot holding
|
|
7164
|
+
* nothing but whitespace is emptied first, so the label it becomes is
|
|
7165
|
+
* `:empty` and takes no row.
|
|
7086
7166
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
7087
7167
|
* @returns {void | Promise<void>}
|
|
7088
7168
|
*/
|
|
7089
7169
|
render(conf) {
|
|
7170
|
+
if (conf.slots?.default && Fragments.isBlank(conf.slots.default)) {
|
|
7171
|
+
conf.slots.default.replaceChildren();
|
|
7172
|
+
}
|
|
7090
7173
|
const built = /** @type {any} */ (this._build(conf));
|
|
7091
7174
|
if (built instanceof Promise) {
|
|
7092
7175
|
return built.then((pieces) => this.#wire(pieces));
|
|
@@ -8426,7 +8509,8 @@ var fml = (function (exports) {
|
|
|
8426
8509
|
* size and count limits it declares.
|
|
8427
8510
|
*
|
|
8428
8511
|
* The `dropzone` and `item-list` attributes show the dropzone and the list of
|
|
8429
|
-
* chosen files
|
|
8512
|
+
* chosen files, and `no-input` takes the picker control out of sight, leaving
|
|
8513
|
+
* it to the keyboard and to assistive technology. A `dropzone` slot replaces the default dropzone's content, and
|
|
8430
8514
|
* an `items` slot holding a template replaces the stock item, rendered with the
|
|
8431
8515
|
* same `files` overlay so that it reads the File objects it lists. Clicking the
|
|
8432
8516
|
* dropzone opens the picker, and the host carries a `dragover` attribute while
|
|
@@ -8469,6 +8553,7 @@ var fml = (function (exports) {
|
|
|
8469
8553
|
'multiple:presence',
|
|
8470
8554
|
'item-list:presence',
|
|
8471
8555
|
'dropzone:presence',
|
|
8556
|
+
'no-input:presence',
|
|
8472
8557
|
'max-files:number',
|
|
8473
8558
|
'max-file-size:number',
|
|
8474
8559
|
'max-total-size:number',
|
|
@@ -8720,7 +8805,7 @@ var fml = (function (exports) {
|
|
|
8720
8805
|
* @returns {string[]|string|null}
|
|
8721
8806
|
*/
|
|
8722
8807
|
get value() {
|
|
8723
|
-
const names = Array.from(this.
|
|
8808
|
+
const names = Array.from(this.files).map((f) => f.name);
|
|
8724
8809
|
return this.multiple ? names : (names[0] ?? null);
|
|
8725
8810
|
}
|
|
8726
8811
|
/**
|
|
@@ -8743,7 +8828,7 @@ var fml = (function (exports) {
|
|
|
8743
8828
|
* The size of the selected files together, in bytes.
|
|
8744
8829
|
* @returns {number}
|
|
8745
8830
|
*/
|
|
8746
|
-
get
|
|
8831
|
+
get totalSize() {
|
|
8747
8832
|
return Array.from(this.files).reduce((a, f) => a + f.size, 0);
|
|
8748
8833
|
}
|
|
8749
8834
|
#maxFiles;
|
|
@@ -8818,6 +8903,23 @@ var fml = (function (exports) {
|
|
|
8818
8903
|
this.#useDropzone = v;
|
|
8819
8904
|
this.reflectTo('dropzone', v);
|
|
8820
8905
|
}
|
|
8906
|
+
#noInput;
|
|
8907
|
+
/**
|
|
8908
|
+
* Whether the picker control is taken out of sight, the dropzone and the
|
|
8909
|
+
* item list carrying the field alone; the stylesheet hides it from the
|
|
8910
|
+
* reflected `no-input` attribute. The control stays in the tab order and
|
|
8911
|
+
* keeps the label, the description and the error, and the dropzone shows
|
|
8912
|
+
* the focus ring while it holds the focus.
|
|
8913
|
+
* @returns {boolean}
|
|
8914
|
+
*/
|
|
8915
|
+
get noInput() {
|
|
8916
|
+
return this.#noInput;
|
|
8917
|
+
}
|
|
8918
|
+
/** @param {boolean} v */
|
|
8919
|
+
set noInput(v) {
|
|
8920
|
+
this.#noInput = v;
|
|
8921
|
+
this.reflectTo('no-input', v);
|
|
8922
|
+
}
|
|
8821
8923
|
}
|
|
8822
8924
|
|
|
8823
8925
|
/** the viewport's breathing room when clamping, in pixels */
|
|
@@ -10581,9 +10683,7 @@ var fml = (function (exports) {
|
|
|
10581
10683
|
static ROLE = 'radiogroup';
|
|
10582
10684
|
static template = `
|
|
10583
10685
|
<fieldset>
|
|
10584
|
-
<legend>
|
|
10585
|
-
{{{{ slots.default }}}}
|
|
10586
|
-
</legend>
|
|
10686
|
+
<legend>{{{{ slots.default }}}}</legend>
|
|
10587
10687
|
<header data-tpl-if="slots.header">
|
|
10588
10688
|
{{{{ slots.header }}}}
|
|
10589
10689
|
</header>
|
|
@@ -10631,6 +10731,9 @@ var fml = (function (exports) {
|
|
|
10631
10731
|
radioEls.forEach((el) => {
|
|
10632
10732
|
el.remove();
|
|
10633
10733
|
});
|
|
10734
|
+
if (Fragments.isBlank(slots.default)) {
|
|
10735
|
+
slots.default.replaceChildren();
|
|
10736
|
+
}
|
|
10634
10737
|
const fragment = this.template().withOverlay({ name, slots, inputsAndLabels }).render();
|
|
10635
10738
|
this.#fieldset = /** @type HTMLElement */ (fragment.firstElementChild);
|
|
10636
10739
|
this.#firstRadio = fragment.querySelector('input[type=radio]');
|
|
@@ -10772,6 +10875,10 @@ var fml = (function (exports) {
|
|
|
10772
10875
|
* @typedef {{ data: any[], size: number }} TablePageResponse the rows of the
|
|
10773
10876
|
* requested page and the total number of rows across every page
|
|
10774
10877
|
*/
|
|
10878
|
+
/**
|
|
10879
|
+
* @typedef {{ count: Element, appending: Element, failure: Element, error: Element }} TableStatus the pieces
|
|
10880
|
+
* of the status bar `paging="infinite"` and `paging="virtual"` show in place of the pager
|
|
10881
|
+
*/
|
|
10775
10882
|
/**
|
|
10776
10883
|
* @typedef {{ load(pageRequest: TablePageRequest, sortRequest: TableSortRequest|null, filterRequest: Record<string, any>): Promise<TablePageResponse> }} TableRowLoader
|
|
10777
10884
|
*/
|
|
@@ -10786,6 +10893,10 @@ var fml = (function (exports) {
|
|
|
10786
10893
|
* current `order` in the cycle `asc`, `desc`, `null`. The button does not
|
|
10787
10894
|
* change its own `order`: whoever handles the event sets it, which `ful-table`
|
|
10788
10895
|
* does once the sorted page has loaded.
|
|
10896
|
+
*
|
|
10897
|
+
* The stylesheet draws an up arrow for `asc` and a down arrow for `desc`. An
|
|
10898
|
+
* unsorted button shows the up arrow dimmed, `asc` being the order its first
|
|
10899
|
+
* activation asks for.
|
|
10789
10900
|
*/
|
|
10790
10901
|
class SortButton extends ParsedElement {
|
|
10791
10902
|
static attributes = ['sorter'];
|
|
@@ -11240,6 +11351,21 @@ var fml = (function (exports) {
|
|
|
11240
11351
|
* for it. Without it the table shows an initial panel until `load`,
|
|
11241
11352
|
* `reload` or `resetWithFilter` is called.
|
|
11242
11353
|
*
|
|
11354
|
+
* `paging="infinite"`, read once at the upgrade, replaces the pager with a
|
|
11355
|
+
* status bar saying how many rows of how many are shown, with a reload
|
|
11356
|
+
* button. A load still replaces the rows with one page, scrolling the rows
|
|
11357
|
+
* back to the top; from then on, whenever a row of the last half page
|
|
11358
|
+
* comes into view, the next page is asked with the same size, sort and
|
|
11359
|
+
* filters and appended (see `load`). `reload`, a sort, a filter submit and a
|
|
11360
|
+
* size change start over from the first page.
|
|
11361
|
+
*
|
|
11362
|
+
* `paging="virtual"` shows the same status bar, saying which rows of how many
|
|
11363
|
+
* are in view, and keeps in the page only the page in view and one on either
|
|
11364
|
+
* side of it, the others standing in as empty space sized on the height of
|
|
11365
|
+
* the rows. The rows must scroll inside the table, as `ful-fill` makes them:
|
|
11366
|
+
* a table whose rows the page scrolls appends as `paging="infinite"` does,
|
|
11367
|
+
* and warns. See `load` and `reload`.
|
|
11368
|
+
*
|
|
11243
11369
|
* Inside the table, and not inside a `ful-table` nested in it, a
|
|
11244
11370
|
* `page:requested` loads that page with the current size,
|
|
11245
11371
|
* sort and filters; a `sort:requested` loads the current page with the new
|
|
@@ -11255,13 +11381,14 @@ var fml = (function (exports) {
|
|
|
11255
11381
|
*/
|
|
11256
11382
|
class Table extends ParsedElement {
|
|
11257
11383
|
/** Read once at the upgrade. */
|
|
11258
|
-
static attributes = ['loader', 'autoload:presence'];
|
|
11384
|
+
static attributes = ['loader', 'autoload:presence', 'paging'];
|
|
11259
11385
|
/** These stay live after the upgrade: see `pageSize`, `src` and `method`. */
|
|
11260
11386
|
static observed = ['page-size:number', 'src', 'method'];
|
|
11261
11387
|
static slots = true;
|
|
11262
11388
|
static config = {
|
|
11263
11389
|
searchIcon: 'search',
|
|
11264
11390
|
emptyIcon: 'inbox',
|
|
11391
|
+
reloadIcon: 'arrow-clockwise',
|
|
11265
11392
|
};
|
|
11266
11393
|
static template = `
|
|
11267
11394
|
<ful-form data-tpl-if="slots.filters">
|
|
@@ -11271,7 +11398,9 @@ var fml = (function (exports) {
|
|
|
11271
11398
|
<table>
|
|
11272
11399
|
<caption data-tpl-if="slots.caption">{{{{ slots.caption }}}}</caption>
|
|
11273
11400
|
<thead></thead>
|
|
11401
|
+
<tbody data-tpl-if="virtual" data-ref="before" aria-hidden="true"><tr><td data-tpl-colspan="schema.length"></td></tr></tbody>
|
|
11274
11402
|
<tbody></tbody>
|
|
11403
|
+
<tbody data-tpl-if="virtual" data-ref="after" aria-hidden="true"><tr><td data-tpl-colspan="schema.length"></td></tr></tbody>
|
|
11275
11404
|
<tbody data-ref="initial">
|
|
11276
11405
|
<tr>
|
|
11277
11406
|
<td data-tpl-colspan="schema.length">
|
|
@@ -11315,7 +11444,16 @@ var fml = (function (exports) {
|
|
|
11315
11444
|
</tfoot>
|
|
11316
11445
|
</table>
|
|
11317
11446
|
</ful-table-wrapper>
|
|
11318
|
-
<ful-pagination current="0" total="1"></ful-pagination>
|
|
11447
|
+
<ful-pagination data-tpl-if="!scrolled" current="0" total="1"></ful-pagination>
|
|
11448
|
+
<ful-table-status data-tpl-if="scrolled">
|
|
11449
|
+
<span data-ref="count"></span>
|
|
11450
|
+
<ful-spinner data-ref="appending" role="status" hidden><span class="ful-sr-only">{{ #l10n:t('spinner.loading') }}</span></ful-spinner>
|
|
11451
|
+
<div data-ref="append-failure" role="alert" hidden>
|
|
11452
|
+
<span>{{ #l10n:t('table.error') }} <span data-ref="append-error"></span></span>
|
|
11453
|
+
<button type="button" data-ref="retry">{{ #l10n:t('table.retry') }}</button>
|
|
11454
|
+
</div>
|
|
11455
|
+
<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>
|
|
11456
|
+
</ful-table-status>
|
|
11319
11457
|
`;
|
|
11320
11458
|
static templates = {
|
|
11321
11459
|
row: `
|
|
@@ -11333,6 +11471,31 @@ var fml = (function (exports) {
|
|
|
11333
11471
|
#feedback;
|
|
11334
11472
|
#paginator;
|
|
11335
11473
|
#sorters;
|
|
11474
|
+
#wrapper;
|
|
11475
|
+
/** @type {TableStatus|null} null under the pager */
|
|
11476
|
+
#status = null;
|
|
11477
|
+
/** @type {IntersectionObserver|null} */
|
|
11478
|
+
#endWatch = null;
|
|
11479
|
+
/** @type {Element[]} the rows whose coming into view appends the next page, none while no append is awaited */
|
|
11480
|
+
#watched = [];
|
|
11481
|
+
/** the page the next append asks for, the rows shown, the total the last answer reported and the rows it carried */
|
|
11482
|
+
#appended = { next: 1, shown: 0, total: 0, last: 0 };
|
|
11483
|
+
/** whether the table keeps a window of pages, `paging="virtual"` over rows that scroll inside it */
|
|
11484
|
+
#virtual = false;
|
|
11485
|
+
/** @type {{ before: HTMLElement, after: HTMLElement } | null} the cells standing in for the rows above and below the window */
|
|
11486
|
+
#spacers = null;
|
|
11487
|
+
/** @type {{ first: number, last: number, pages: Map<number, Element[]> } | null} the pages in the page, in order, null while no rows show */
|
|
11488
|
+
#window = null;
|
|
11489
|
+
/** the total number of rows the last answer reported */
|
|
11490
|
+
#total = 0;
|
|
11491
|
+
/** the height of a row the spacers are sized on, measured on the first rows a load shows; 0 until measured */
|
|
11492
|
+
#rowHeight = 0;
|
|
11493
|
+
/** @type {{ readonly stale: boolean } | null} the claim of the window shift in flight */
|
|
11494
|
+
#shifting = null;
|
|
11495
|
+
/** whether the last window shift failed: scrolling asks for nothing more until a retry or a load */
|
|
11496
|
+
#windowFailed = false;
|
|
11497
|
+
/** @type {ReturnType<typeof setTimeout>|undefined} */
|
|
11498
|
+
#jump = undefined;
|
|
11336
11499
|
/** @type {{ pageRequest: { page: number, size: number }, sortRequest: any, filterRequest: any }} */
|
|
11337
11500
|
#latestRequest = { pageRequest: { page: 0, size: 10 }, sortRequest: null, filterRequest: {} };
|
|
11338
11501
|
/** whether a load has been asked for, by autoload or by a caller */
|
|
@@ -11362,18 +11525,19 @@ var fml = (function (exports) {
|
|
|
11362
11525
|
* writing the size is not a request to start loading, which is what
|
|
11363
11526
|
* `autoload` and `reload()` are for. A write that does not change the size
|
|
11364
11527
|
* does nothing. A write made before the render is replaced by the declared
|
|
11365
|
-
* `page-size` when the table renders
|
|
11528
|
+
* `page-size` when the table renders, or by ten when that is not a number
|
|
11529
|
+
* above zero.
|
|
11366
11530
|
*
|
|
11367
11531
|
* The reload is not awaited: a failure shows the error panel and its
|
|
11368
11532
|
* rejection is unhandled.
|
|
11369
11533
|
* @param {number|string|null|undefined} value a number or a string of
|
|
11370
11534
|
* one, such as a select's value; absent or null is the default of ten,
|
|
11371
11535
|
* so removing the attribute restores it, and a value that is not a
|
|
11372
|
-
* number is ignored
|
|
11536
|
+
* finite number above zero is ignored
|
|
11373
11537
|
*/
|
|
11374
11538
|
set pageSize(value) {
|
|
11375
11539
|
const size = Number(value ?? 10);
|
|
11376
|
-
if (Number.
|
|
11540
|
+
if (!Number.isFinite(size) || size <= 0 || size === this.#latestRequest.pageRequest.size) {
|
|
11377
11541
|
return;
|
|
11378
11542
|
}
|
|
11379
11543
|
this.#latestRequest = { ...this.#latestRequest, pageRequest: { page: 0, size } };
|
|
@@ -11459,6 +11623,7 @@ var fml = (function (exports) {
|
|
|
11459
11623
|
}
|
|
11460
11624
|
this.#buildLoader();
|
|
11461
11625
|
this.#latestRequest = { ...this.#latestRequest, pageRequest: { ...this.#latestRequest.pageRequest, page: 0 } };
|
|
11626
|
+
this.#window = null;
|
|
11462
11627
|
return true;
|
|
11463
11628
|
}
|
|
11464
11629
|
/**
|
|
@@ -11469,14 +11634,24 @@ var fml = (function (exports) {
|
|
|
11469
11634
|
async render({ slots }) {
|
|
11470
11635
|
const template = this.template();
|
|
11471
11636
|
const schema = TableSchemaParser.parse(slots.schema, template);
|
|
11472
|
-
const
|
|
11637
|
+
const paging = this.declared('paging');
|
|
11638
|
+
const fragment = template
|
|
11639
|
+
.withOverlay({
|
|
11640
|
+
slots,
|
|
11641
|
+
schema,
|
|
11642
|
+
scrolled: paging === 'infinite' || paging === 'virtual',
|
|
11643
|
+
virtual: paging === 'virtual',
|
|
11644
|
+
})
|
|
11645
|
+
.render();
|
|
11473
11646
|
const tableWrapper = /** @type HTMLTableElement */ (Nodes.queryChildren(fragment, 'ful-table-wrapper'));
|
|
11474
11647
|
const table = /** @type HTMLTableElement */ (tableWrapper.querySelector('table'));
|
|
11475
11648
|
Attributes.forward('table-', this, table);
|
|
11476
11649
|
this.#buildLoader();
|
|
11477
11650
|
|
|
11651
|
+
this.#wrapper = tableWrapper;
|
|
11652
|
+
const status = Nodes.queryChildren(fragment, 'ful-table-status');
|
|
11478
11653
|
this.#schema = schema;
|
|
11479
|
-
this.#body = table.querySelector(':scope > tbody');
|
|
11654
|
+
this.#body = table.querySelector(':scope > tbody:not([data-ref])');
|
|
11480
11655
|
this.#loading = table.querySelector(':scope > tbody[data-ref=loading]');
|
|
11481
11656
|
this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
|
|
11482
11657
|
this.#empty = table.querySelector(':scope > tbody[data-ref=empty]');
|
|
@@ -11486,16 +11661,67 @@ var fml = (function (exports) {
|
|
|
11486
11661
|
this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
|
|
11487
11662
|
this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
|
|
11488
11663
|
this.replaceChildren(fragment);
|
|
11664
|
+
if (paging === 'virtual') {
|
|
11665
|
+
const overflow = getComputedStyle(tableWrapper).overflowY;
|
|
11666
|
+
this.#virtual = overflow === 'auto' || overflow === 'scroll';
|
|
11667
|
+
if (!this.#virtual) {
|
|
11668
|
+
console.warn(
|
|
11669
|
+
'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',
|
|
11670
|
+
);
|
|
11671
|
+
}
|
|
11672
|
+
}
|
|
11673
|
+
if (status) {
|
|
11674
|
+
const piece = (/** @type string */ ref) =>
|
|
11675
|
+
/** @type Element */ (status.querySelector(`[data-ref=${ref}]`));
|
|
11676
|
+
this.#status = {
|
|
11677
|
+
count: piece('count'),
|
|
11678
|
+
appending: piece('appending'),
|
|
11679
|
+
failure: piece('append-failure'),
|
|
11680
|
+
error: piece('append-error'),
|
|
11681
|
+
};
|
|
11682
|
+
piece('reload').addEventListener('click', () => this.reload());
|
|
11683
|
+
piece('retry').addEventListener('click', () => {
|
|
11684
|
+
if (!this.#virtual) {
|
|
11685
|
+
this.#appendNext();
|
|
11686
|
+
return;
|
|
11687
|
+
}
|
|
11688
|
+
this.#windowFailed = false;
|
|
11689
|
+
this.#reconcile(true);
|
|
11690
|
+
});
|
|
11691
|
+
}
|
|
11692
|
+
if (this.#virtual) {
|
|
11693
|
+
this.#spacers = {
|
|
11694
|
+
before: /** @type HTMLElement */ (table.querySelector(':scope > tbody[data-ref=before] td')),
|
|
11695
|
+
after: /** @type HTMLElement */ (table.querySelector(':scope > tbody[data-ref=after] td')),
|
|
11696
|
+
};
|
|
11697
|
+
// scroll fires once per frame at most, so it needs no throttle of its own
|
|
11698
|
+
tableWrapper.addEventListener(
|
|
11699
|
+
'scroll',
|
|
11700
|
+
() => {
|
|
11701
|
+
this.#countInView();
|
|
11702
|
+
this.#reconcile(false);
|
|
11703
|
+
},
|
|
11704
|
+
{ passive: true },
|
|
11705
|
+
);
|
|
11706
|
+
} else if (status) {
|
|
11707
|
+
this.#endWatch = new IntersectionObserver((entries) => {
|
|
11708
|
+
// an entry queued before the watch moved on to another row, or stopped, asks for nothing
|
|
11709
|
+
if (entries.some((e) => e.isIntersecting && this.#watched.includes(e.target))) {
|
|
11710
|
+
this.#appendNext();
|
|
11711
|
+
}
|
|
11712
|
+
});
|
|
11713
|
+
}
|
|
11489
11714
|
const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
|
|
11490
11715
|
schema.headersTemplate.renderTo(thead);
|
|
11491
11716
|
this.#sorters = thead.querySelectorAll('ful-sorter');
|
|
11492
11717
|
await Rendering.waitForChildren(this);
|
|
11493
11718
|
|
|
11494
11719
|
const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
|
|
11720
|
+
const declaredSize = this.declared('page-size');
|
|
11495
11721
|
this.#latestRequest = {
|
|
11496
11722
|
pageRequest: {
|
|
11497
11723
|
page: 0,
|
|
11498
|
-
size:
|
|
11724
|
+
size: Number.isFinite(declaredSize) && declaredSize > 0 ? declaredSize : 10,
|
|
11499
11725
|
},
|
|
11500
11726
|
sortRequest: schema.sort,
|
|
11501
11727
|
filterRequest: maybeForm?.values ?? {},
|
|
@@ -11541,6 +11767,14 @@ var fml = (function (exports) {
|
|
|
11541
11767
|
* `submit` listeners as the later ones, and the load its `submit:success`
|
|
11542
11768
|
* starts is the one awaited. A filter submit that is cancelled, fails or
|
|
11543
11769
|
* is dropped for one in flight loads nothing, and the promise resolves.
|
|
11770
|
+
*
|
|
11771
|
+
* Under `paging="virtual"`, once rows show, it asks again for the pages in
|
|
11772
|
+
* the page, with the same size, sort and filters, and shows them where
|
|
11773
|
+
* they are, the scroll position and the widths of the columns kept; the
|
|
11774
|
+
* table is busy and settles, fails and dispatches as a load does, one
|
|
11775
|
+
* `load:success` per page. When the answers report fewer rows than the
|
|
11776
|
+
* window starts at, it loads the first page instead. A `src` or `method`
|
|
11777
|
+
* write still starts over from the first page.
|
|
11544
11778
|
* @returns {Promise<void>} as `load`
|
|
11545
11779
|
*/
|
|
11546
11780
|
async reload() {
|
|
@@ -11550,6 +11784,9 @@ var fml = (function (exports) {
|
|
|
11550
11784
|
}
|
|
11551
11785
|
return;
|
|
11552
11786
|
}
|
|
11787
|
+
if (this.#window) {
|
|
11788
|
+
return await this.#reloadWindow();
|
|
11789
|
+
}
|
|
11553
11790
|
return await this.load(
|
|
11554
11791
|
this.#latestRequest.pageRequest,
|
|
11555
11792
|
this.#latestRequest.sortRequest,
|
|
@@ -11579,12 +11816,49 @@ var fml = (function (exports) {
|
|
|
11579
11816
|
* `aria-busy` is removed, `load:failure` is dispatched (bubbling, not
|
|
11580
11817
|
* cancelable) with `detail` set to
|
|
11581
11818
|
* `{ pageRequest, sortRequest, filterRequest, exception }`, and the
|
|
11582
|
-
* returned promise rejects with the same error.
|
|
11819
|
+
* returned promise rejects with the same error. An answer the table cannot
|
|
11820
|
+
* page fails the same way, with an error saying so: one whose `size` is
|
|
11821
|
+
* not a number, as a response of another shape read without a
|
|
11822
|
+
* `response-mapper` is, or a page size of zero.
|
|
11583
11823
|
*
|
|
11584
11824
|
* A load started after this one supersedes it: when this one's answer or
|
|
11585
11825
|
* failure arrives it changes nothing, dispatches nothing and resolves;
|
|
11586
11826
|
* superseded while its rows render, it leaves them to the newer load and
|
|
11587
11827
|
* dispatches nothing.
|
|
11828
|
+
*
|
|
11829
|
+
* Under `paging="infinite"` the status bar takes the pager's place, and
|
|
11830
|
+
* once the load has dispatched `load:success` the next page is awaited:
|
|
11831
|
+
* when a row of the last half page comes into view, through
|
|
11832
|
+
* whatever scrolls it, the following page is asked for with the same
|
|
11833
|
+
* size, sort and filters and its rows are appended. An append runs
|
|
11834
|
+
* without `aria-busy`, a spinner in the status bar standing in, and
|
|
11835
|
+
* dispatches `load:success` or `load:failure` as a load does, its
|
|
11836
|
+
* `pageRequest` naming the appended page. A failed append keeps the
|
|
11837
|
+
* rows, shows the error in the status bar with a retry button and
|
|
11838
|
+
* rejects to nobody, nothing awaiting it. Appending stops once the rows
|
|
11839
|
+
* shown reach the total the last answer reported, or an answer carries
|
|
11840
|
+
* no rows. A load supersedes an append in flight; `reload` repeats this
|
|
11841
|
+
* load, not the pages appended to it.
|
|
11842
|
+
*
|
|
11843
|
+
* Under `paging="virtual"` the load shows its page as the window, scrolled
|
|
11844
|
+
* to where it sits among the total, and measures two things on its rows:
|
|
11845
|
+
* the height the empty space standing in for the other rows is sized on,
|
|
11846
|
+
* and the width of the columns, which then stay as they are, in
|
|
11847
|
+
* proportion, until the next load, so that the pages coming and going do
|
|
11848
|
+
* not move them. From then on the window follows the scroll: the page in
|
|
11849
|
+
* view and one on either side stay in the page and the others are dropped,
|
|
11850
|
+
* the page holding the focus kept while it is within two pages of them. A
|
|
11851
|
+
* page next to the window is asked for at once; a scroll landing farther
|
|
11852
|
+
* waits for the scroll to rest. Window pages are asked for without
|
|
11853
|
+
* `aria-busy`, a spinner in the status bar standing in, and dispatch
|
|
11854
|
+
* `load:success` or `load:failure` per page as appends do; a failure keeps
|
|
11855
|
+
* the rows and offers a retry. The row in view stays where it is as pages
|
|
11856
|
+
* come and go above it: the table measures it before and after and
|
|
11857
|
+
* corrects the scroll by whatever the browser's own scroll anchoring has
|
|
11858
|
+
* not kept, which is all of it where the browser anchors no table rows
|
|
11859
|
+
* (Firefox), has no scroll anchoring or the rows' container declares
|
|
11860
|
+
* `overflow-anchor: none`, and nothing where it has. The table carries
|
|
11861
|
+
* `aria-rowcount` and each row its `aria-rowindex`.
|
|
11588
11862
|
* @param {TablePageRequest} pageRequest
|
|
11589
11863
|
* @param {TableSortRequest|null} sortRequest
|
|
11590
11864
|
* @param {Record<string, any>} filterRequest
|
|
@@ -11595,6 +11869,13 @@ var fml = (function (exports) {
|
|
|
11595
11869
|
this.#loadRequested = true;
|
|
11596
11870
|
this.#requestedFilters = filterRequest;
|
|
11597
11871
|
const claim = this.#loads.take();
|
|
11872
|
+
this.#watched = [];
|
|
11873
|
+
this.#endWatch?.disconnect();
|
|
11874
|
+
clearTimeout(this.#jump);
|
|
11875
|
+
this.#shifting = null;
|
|
11876
|
+
this.#windowFailed = false;
|
|
11877
|
+
this.#status?.appending.setAttribute('hidden', '');
|
|
11878
|
+
this.#status?.failure.setAttribute('hidden', '');
|
|
11598
11879
|
this.#loading.toggleAttribute('hidden', this.#body.childElementCount > 0);
|
|
11599
11880
|
this.#feedback.setAttribute('hidden', '');
|
|
11600
11881
|
this.#noAutoload.setAttribute('hidden', '');
|
|
@@ -11607,6 +11888,11 @@ var fml = (function (exports) {
|
|
|
11607
11888
|
return;
|
|
11608
11889
|
}
|
|
11609
11890
|
lastPage = Math.max(0, Math.ceil(pageResponse.size / pageRequest.size) - 1);
|
|
11891
|
+
if (!Number.isFinite(lastPage)) {
|
|
11892
|
+
throw new Error(
|
|
11893
|
+
`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`,
|
|
11894
|
+
);
|
|
11895
|
+
}
|
|
11610
11896
|
if (pageRequest.page <= lastPage) {
|
|
11611
11897
|
this.#latestRequest = { pageRequest, sortRequest, filterRequest };
|
|
11612
11898
|
this.#loaded = true;
|
|
@@ -11619,6 +11905,9 @@ var fml = (function (exports) {
|
|
|
11619
11905
|
if (claim.stale) {
|
|
11620
11906
|
return;
|
|
11621
11907
|
}
|
|
11908
|
+
if (this.#virtual) {
|
|
11909
|
+
this.#settleWindow();
|
|
11910
|
+
}
|
|
11622
11911
|
this.removeAttribute('aria-busy');
|
|
11623
11912
|
this.dispatchEvent(
|
|
11624
11913
|
new CustomEvent('load:success', {
|
|
@@ -11627,24 +11916,15 @@ var fml = (function (exports) {
|
|
|
11627
11916
|
detail: { pageRequest, sortRequest, filterRequest, response: pageResponse },
|
|
11628
11917
|
}),
|
|
11629
11918
|
);
|
|
11919
|
+
this.#watchEnd(pageRequest.size);
|
|
11920
|
+
this.#reconcile(false);
|
|
11630
11921
|
return;
|
|
11631
11922
|
}
|
|
11632
11923
|
} catch (/** @type any */ error) {
|
|
11633
11924
|
if (claim.stale) {
|
|
11634
11925
|
return;
|
|
11635
11926
|
}
|
|
11636
|
-
this.#
|
|
11637
|
-
this.#body.replaceChildren();
|
|
11638
|
-
this.#feedback.removeAttribute('hidden');
|
|
11639
|
-
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = Localization.of().failure(error);
|
|
11640
|
-
this.removeAttribute('aria-busy');
|
|
11641
|
-
this.dispatchEvent(
|
|
11642
|
-
new CustomEvent('load:failure', {
|
|
11643
|
-
bubbles: true,
|
|
11644
|
-
cancelable: false,
|
|
11645
|
-
detail: { pageRequest, sortRequest, filterRequest, exception: error },
|
|
11646
|
-
}),
|
|
11647
|
-
);
|
|
11927
|
+
this.#fail(error, { pageRequest, sortRequest, filterRequest });
|
|
11648
11928
|
throw error;
|
|
11649
11929
|
}
|
|
11650
11930
|
return await this.load({ page: lastPage, size: pageRequest.size }, sortRequest, filterRequest);
|
|
@@ -11675,19 +11955,471 @@ var fml = (function (exports) {
|
|
|
11675
11955
|
filterRequest,
|
|
11676
11956
|
);
|
|
11677
11957
|
}
|
|
11678
|
-
|
|
11679
|
-
|
|
11958
|
+
/**
|
|
11959
|
+
* Watches the last half page of rows, so that any of them coming into
|
|
11960
|
+
* view appends the next page, unless the rows shown already reach the
|
|
11961
|
+
* total or the last answer carried none. A single row would be missed by
|
|
11962
|
+
* a scroll jumping past it, to the end for instance.
|
|
11963
|
+
* @param {number} size the rows a page asks for
|
|
11964
|
+
*/
|
|
11965
|
+
#watchEnd(size) {
|
|
11966
|
+
const { shown, total, last } = this.#appended;
|
|
11967
|
+
if (!this.#endWatch || !(shown < total) || last === 0) {
|
|
11968
|
+
return;
|
|
11969
|
+
}
|
|
11970
|
+
const rows = [...this.#body.children];
|
|
11971
|
+
this.#watched = rows.slice(Math.max(0, rows.length - Math.ceil(size / 2)));
|
|
11972
|
+
for (const row of this.#watched) {
|
|
11973
|
+
this.#endWatch.observe(row);
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11976
|
+
async #appendNext() {
|
|
11977
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
11978
|
+
this.#watched = [];
|
|
11979
|
+
this.#endWatch?.disconnect();
|
|
11980
|
+
const { sortRequest, filterRequest } = this.#latestRequest;
|
|
11981
|
+
const pageRequest = { page: this.#appended.next, size: this.#latestRequest.pageRequest.size };
|
|
11982
|
+
const claim = this.#loads.take();
|
|
11983
|
+
status.failure.setAttribute('hidden', '');
|
|
11984
|
+
status.appending.removeAttribute('hidden');
|
|
11985
|
+
try {
|
|
11986
|
+
const pageResponse = await this.#loader.load(pageRequest, sortRequest, filterRequest);
|
|
11987
|
+
if (claim.stale) {
|
|
11988
|
+
return;
|
|
11989
|
+
}
|
|
11990
|
+
this.#body.append(
|
|
11991
|
+
this.template('row')
|
|
11992
|
+
.withOverlay({ schema: this.#schema, pageRequest, filterRequest, pageResponse })
|
|
11993
|
+
.render(),
|
|
11994
|
+
);
|
|
11995
|
+
this.#appended = {
|
|
11996
|
+
next: pageRequest.page + 1,
|
|
11997
|
+
shown: this.#appended.shown + pageResponse.data.length,
|
|
11998
|
+
total: pageResponse.size,
|
|
11999
|
+
last: pageResponse.data.length,
|
|
12000
|
+
};
|
|
12001
|
+
status.count.textContent = Localization.of().t('table.loaded', {
|
|
12002
|
+
loaded: this.#appended.shown,
|
|
12003
|
+
total: this.#appended.total,
|
|
12004
|
+
});
|
|
12005
|
+
await Rendering.waitForChildren(this.#body).catch(() => undefined);
|
|
12006
|
+
if (claim.stale) {
|
|
12007
|
+
return;
|
|
12008
|
+
}
|
|
12009
|
+
status.appending.setAttribute('hidden', '');
|
|
12010
|
+
this.dispatchEvent(
|
|
12011
|
+
new CustomEvent('load:success', {
|
|
12012
|
+
bubbles: true,
|
|
12013
|
+
cancelable: false,
|
|
12014
|
+
detail: { pageRequest, sortRequest, filterRequest, response: pageResponse },
|
|
12015
|
+
}),
|
|
12016
|
+
);
|
|
12017
|
+
this.#watchEnd(pageRequest.size);
|
|
12018
|
+
} catch (/** @type any */ error) {
|
|
12019
|
+
if (claim.stale) {
|
|
12020
|
+
return;
|
|
12021
|
+
}
|
|
12022
|
+
status.appending.setAttribute('hidden', '');
|
|
12023
|
+
status.error.textContent = Localization.of().failure(error);
|
|
12024
|
+
status.failure.removeAttribute('hidden');
|
|
12025
|
+
this.dispatchEvent(
|
|
12026
|
+
new CustomEvent('load:failure', {
|
|
12027
|
+
bubbles: true,
|
|
12028
|
+
cancelable: false,
|
|
12029
|
+
detail: { pageRequest, sortRequest, filterRequest, exception: error },
|
|
12030
|
+
}),
|
|
12031
|
+
);
|
|
12032
|
+
}
|
|
12033
|
+
}
|
|
12034
|
+
/**
|
|
12035
|
+
* Renders the rows of a page, numbering them with `aria-rowindex` under
|
|
12036
|
+
* `paging="virtual"`, the header row being the first.
|
|
12037
|
+
* @param {TablePageRequest} pageRequest
|
|
12038
|
+
* @param {Record<string, any>} filterRequest
|
|
12039
|
+
* @param {TablePageResponse} pageResponse
|
|
12040
|
+
* @returns {Element[]}
|
|
12041
|
+
*/
|
|
12042
|
+
#pageRows(pageRequest, filterRequest, pageResponse) {
|
|
12043
|
+
const rows = [
|
|
12044
|
+
...this.template('row')
|
|
12045
|
+
.withOverlay({ schema: this.#schema, pageRequest, filterRequest, pageResponse })
|
|
12046
|
+
.render().children,
|
|
12047
|
+
];
|
|
12048
|
+
if (this.#virtual) {
|
|
12049
|
+
rows.forEach((row, i) => {
|
|
12050
|
+
row.setAttribute('aria-rowindex', String(pageRequest.page * pageRequest.size + i + 2));
|
|
12051
|
+
});
|
|
12052
|
+
}
|
|
12053
|
+
return rows;
|
|
12054
|
+
}
|
|
12055
|
+
/**
|
|
12056
|
+
* Shows a failed load: the rows go, the error panel shows and
|
|
12057
|
+
* `load:failure` is dispatched.
|
|
12058
|
+
* @param {any} error
|
|
12059
|
+
* @param {{ pageRequest: TablePageRequest, sortRequest: TableSortRequest|null, filterRequest: Record<string, any> }} request
|
|
12060
|
+
*/
|
|
12061
|
+
#fail(error, request) {
|
|
11680
12062
|
this.#loading.setAttribute('hidden', '');
|
|
11681
|
-
this.#body.replaceChildren(
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
12063
|
+
this.#body.replaceChildren();
|
|
12064
|
+
this.#window = null;
|
|
12065
|
+
this.#fitSpacers();
|
|
12066
|
+
this.#status?.count.replaceChildren();
|
|
12067
|
+
this.#feedback.removeAttribute('hidden');
|
|
12068
|
+
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = Localization.of().failure(error);
|
|
12069
|
+
this.removeAttribute('aria-busy');
|
|
12070
|
+
this.dispatchEvent(
|
|
12071
|
+
new CustomEvent('load:failure', {
|
|
12072
|
+
bubbles: true,
|
|
12073
|
+
cancelable: false,
|
|
12074
|
+
detail: { ...request, exception: error },
|
|
12075
|
+
}),
|
|
11690
12076
|
);
|
|
12077
|
+
}
|
|
12078
|
+
/** Counts the rows of a load just shown and moves the window where its page sits. */
|
|
12079
|
+
#settleWindow() {
|
|
12080
|
+
const table = /** @type HTMLTableElement */ (this.#body.parentElement);
|
|
12081
|
+
table.setAttribute('aria-rowcount', String(this.#total + 1));
|
|
12082
|
+
table.tHead?.rows[0]?.setAttribute('aria-rowindex', '1');
|
|
12083
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
12084
|
+
const window = this.#window;
|
|
12085
|
+
if (!window) {
|
|
12086
|
+
this.#fitSpacers();
|
|
12087
|
+
status.count.textContent = Localization.of().t('table.loaded', { loaded: 0, total: this.#total });
|
|
12088
|
+
return;
|
|
12089
|
+
}
|
|
12090
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
12091
|
+
const from = window.first * size;
|
|
12092
|
+
status.count.textContent = Localization.of().t('table.range', {
|
|
12093
|
+
from: from + 1,
|
|
12094
|
+
to: from + /** @type Element[] */ (window.pages.get(window.first)).length,
|
|
12095
|
+
total: this.#total,
|
|
12096
|
+
});
|
|
12097
|
+
if (this.#measureRows()) {
|
|
12098
|
+
this.#wrapper.scrollTop = from * this.#rowHeight;
|
|
12099
|
+
}
|
|
12100
|
+
}
|
|
12101
|
+
/**
|
|
12102
|
+
* Measures the height of a row and locks the widths of the columns, once
|
|
12103
|
+
* per load, on the rows it shows, then sizes the spacers. A table that is not laid out, inside a
|
|
12104
|
+
* closed disclosure for instance, measures nothing and is measured when
|
|
12105
|
+
* its rows next scroll.
|
|
12106
|
+
* @returns {boolean} whether the rows are measured
|
|
12107
|
+
*/
|
|
12108
|
+
#measureRows() {
|
|
12109
|
+
if (this.#rowHeight > 0) {
|
|
12110
|
+
return true;
|
|
12111
|
+
}
|
|
12112
|
+
const count = this.#body.childElementCount;
|
|
12113
|
+
const height = this.#body.getBoundingClientRect().height;
|
|
12114
|
+
if (count === 0 || height === 0) {
|
|
12115
|
+
return false;
|
|
12116
|
+
}
|
|
12117
|
+
this.#rowHeight = height / count;
|
|
12118
|
+
this.#lockColumns();
|
|
12119
|
+
this.#fitSpacers();
|
|
12120
|
+
return true;
|
|
12121
|
+
}
|
|
12122
|
+
/**
|
|
12123
|
+
* Fixes the widths of the columns, in proportion to the table, at those
|
|
12124
|
+
* the rows in the page give them.
|
|
12125
|
+
*/
|
|
12126
|
+
#lockColumns() {
|
|
12127
|
+
const table = /** @type HTMLTableElement */ (this.#body.parentElement);
|
|
12128
|
+
const widths = [...(table.tHead?.rows[0]?.cells ?? [])].map((cell) => cell.getBoundingClientRect().width);
|
|
12129
|
+
const sum = widths.reduce((a, b) => a + b, 0);
|
|
12130
|
+
const columns = document.createElement('colgroup');
|
|
12131
|
+
columns.setAttribute('data-ref', 'widths');
|
|
12132
|
+
for (const width of widths) {
|
|
12133
|
+
const column = document.createElement('col');
|
|
12134
|
+
column.style.width = `${(width / sum) * 100}%`;
|
|
12135
|
+
columns.append(column);
|
|
12136
|
+
}
|
|
12137
|
+
table.insertBefore(columns, table.tHead);
|
|
12138
|
+
}
|
|
12139
|
+
#unlockColumns() {
|
|
12140
|
+
this.#body.parentElement?.querySelector(':scope > colgroup[data-ref=widths]')?.remove();
|
|
12141
|
+
}
|
|
12142
|
+
/** Sizes the space standing in for the rows above and below the window. */
|
|
12143
|
+
#fitSpacers() {
|
|
12144
|
+
if (!this.#spacers) {
|
|
12145
|
+
return;
|
|
12146
|
+
}
|
|
12147
|
+
const window = this.#window;
|
|
12148
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
12149
|
+
const above = window ? window.first * size : 0;
|
|
12150
|
+
const below = window ? Math.max(0, this.#total - (window.last + 1) * size) : 0;
|
|
12151
|
+
this.#spacers.before.style.height = `${above * this.#rowHeight}px`;
|
|
12152
|
+
this.#spacers.after.style.height = `${below * this.#rowHeight}px`;
|
|
12153
|
+
}
|
|
12154
|
+
/** Says in the status bar which rows are in view; nothing changes while none is. */
|
|
12155
|
+
#countInView() {
|
|
12156
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
12157
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
12158
|
+
const view = this.#wrapper.getBoundingClientRect();
|
|
12159
|
+
const top = view.top + (this.#body.parentElement?.querySelector(':scope > thead')?.getBoundingClientRect().height ?? 0);
|
|
12160
|
+
let from = 0;
|
|
12161
|
+
let to = 0;
|
|
12162
|
+
for (const [page, rows] of this.#window?.pages ?? []) {
|
|
12163
|
+
rows.forEach((row, i) => {
|
|
12164
|
+
const rect = row.getBoundingClientRect();
|
|
12165
|
+
if (rect.bottom > top && rect.top < view.bottom) {
|
|
12166
|
+
from ||= page * size + i + 1;
|
|
12167
|
+
to = page * size + i + 1;
|
|
12168
|
+
}
|
|
12169
|
+
});
|
|
12170
|
+
}
|
|
12171
|
+
if (from > 0) {
|
|
12172
|
+
status.count.textContent = Localization.of().t('table.range', { from, to, total: this.#total });
|
|
12173
|
+
}
|
|
12174
|
+
}
|
|
12175
|
+
/**
|
|
12176
|
+
* The page at the middle of the view: found among the rows in the page,
|
|
12177
|
+
* or estimated on the height of a row when the middle falls in the space
|
|
12178
|
+
* standing in for others.
|
|
12179
|
+
* @param {{ first: number, last: number, pages: Map<number, Element[]> }} window
|
|
12180
|
+
* @returns {number}
|
|
12181
|
+
*/
|
|
12182
|
+
#pageInView(window) {
|
|
12183
|
+
const pageHeight = this.#latestRequest.pageRequest.size * this.#rowHeight;
|
|
12184
|
+
const view = this.#wrapper.getBoundingClientRect();
|
|
12185
|
+
const middle = view.top + view.height / 2;
|
|
12186
|
+
const rows = this.#body.getBoundingClientRect();
|
|
12187
|
+
if (middle < rows.top) {
|
|
12188
|
+
return Math.max(0, window.first - Math.ceil((rows.top - middle) / pageHeight));
|
|
12189
|
+
}
|
|
12190
|
+
if (middle >= rows.bottom) {
|
|
12191
|
+
return window.last + 1 + Math.floor((middle - rows.bottom) / pageHeight);
|
|
12192
|
+
}
|
|
12193
|
+
let found = window.first;
|
|
12194
|
+
for (const [page, pageRows] of window.pages) {
|
|
12195
|
+
if (pageRows.length > 0 && pageRows[0].getBoundingClientRect().top <= middle) {
|
|
12196
|
+
found = page;
|
|
12197
|
+
}
|
|
12198
|
+
}
|
|
12199
|
+
return found;
|
|
12200
|
+
}
|
|
12201
|
+
/**
|
|
12202
|
+
* Moves the window to the page in view and its neighbours. A window next
|
|
12203
|
+
* to the current one moves at once; one farther away waits for the scroll
|
|
12204
|
+
* to rest, unless `settled` says it has.
|
|
12205
|
+
* @param {boolean} settled
|
|
12206
|
+
*/
|
|
12207
|
+
#reconcile(settled) {
|
|
12208
|
+
const window = this.#window;
|
|
12209
|
+
const pageCount = Math.ceil(this.#total / this.#latestRequest.pageRequest.size);
|
|
12210
|
+
if (
|
|
12211
|
+
!window ||
|
|
12212
|
+
this.#shifting ||
|
|
12213
|
+
this.#windowFailed ||
|
|
12214
|
+
this.hasAttribute('aria-busy') ||
|
|
12215
|
+
!this.#wrapper.isConnected ||
|
|
12216
|
+
!(pageCount > 0) ||
|
|
12217
|
+
!this.#measureRows()
|
|
12218
|
+
) {
|
|
12219
|
+
return;
|
|
12220
|
+
}
|
|
12221
|
+
const target = Math.min(pageCount - 1, this.#pageInView(window));
|
|
12222
|
+
let first = Math.max(0, target - 1);
|
|
12223
|
+
let last = Math.min(pageCount - 1, target + 1);
|
|
12224
|
+
const focused = this.#body.contains(document.activeElement)
|
|
12225
|
+
? /** @type Element */ (document.activeElement).closest('tr')
|
|
12226
|
+
: null;
|
|
12227
|
+
const focusedPage = [...window.pages].find(([, rows]) => focused && rows.includes(focused))?.[0];
|
|
12228
|
+
if (focusedPage !== undefined && Math.max(last, focusedPage) - Math.min(first, focusedPage) <= 4) {
|
|
12229
|
+
first = Math.min(first, focusedPage);
|
|
12230
|
+
last = Math.max(last, focusedPage);
|
|
12231
|
+
}
|
|
12232
|
+
if (first === window.first && last === window.last) {
|
|
12233
|
+
return;
|
|
12234
|
+
}
|
|
12235
|
+
clearTimeout(this.#jump);
|
|
12236
|
+
if (!settled && (last < window.first - 1 || first > window.last + 1)) {
|
|
12237
|
+
this.#jump = setTimeout(() => this.#reconcile(true), 150);
|
|
12238
|
+
return;
|
|
12239
|
+
}
|
|
12240
|
+
this.#shiftWindow(window, first, last);
|
|
12241
|
+
}
|
|
12242
|
+
/**
|
|
12243
|
+
* Asks for the pages of the new window that are not in the page, drops
|
|
12244
|
+
* the ones outside it and keeps the row in view where it is.
|
|
12245
|
+
* @param {{ first: number, last: number, pages: Map<number, Element[]> }} window
|
|
12246
|
+
* @param {number} first
|
|
12247
|
+
* @param {number} last
|
|
12248
|
+
*/
|
|
12249
|
+
async #shiftWindow(window, first, last) {
|
|
12250
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
12251
|
+
const claim = this.#loads.hold();
|
|
12252
|
+
this.#shifting = claim;
|
|
12253
|
+
const { sortRequest, filterRequest } = this.#latestRequest;
|
|
12254
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
12255
|
+
const wanted = Array.from({ length: last - first + 1 }, (_, i) => first + i);
|
|
12256
|
+
const missing = wanted.filter((page) => !window.pages.has(page));
|
|
12257
|
+
status.failure.setAttribute('hidden', '');
|
|
12258
|
+
status.appending.removeAttribute('hidden');
|
|
12259
|
+
try {
|
|
12260
|
+
const answers = await Promise.all(
|
|
12261
|
+
missing.map(async (page) => {
|
|
12262
|
+
const pageRequest = { page, size };
|
|
12263
|
+
return { pageRequest, response: await this.#loader.load(pageRequest, sortRequest, filterRequest) };
|
|
12264
|
+
}),
|
|
12265
|
+
);
|
|
12266
|
+
if (claim.stale) {
|
|
12267
|
+
return;
|
|
12268
|
+
}
|
|
12269
|
+
const kept = wanted.filter((page) => window.pages.has(page));
|
|
12270
|
+
const top = this.#wrapper.getBoundingClientRect().top;
|
|
12271
|
+
const anchor = kept
|
|
12272
|
+
.flatMap((page) => window.pages.get(page) ?? [])
|
|
12273
|
+
.find((row) => row.getBoundingClientRect().bottom > top);
|
|
12274
|
+
const anchorTop = anchor?.getBoundingClientRect().top ?? 0;
|
|
12275
|
+
for (const [page, rows] of window.pages) {
|
|
12276
|
+
if (page < first || page > last) {
|
|
12277
|
+
for (const row of rows) {
|
|
12278
|
+
row.remove();
|
|
12279
|
+
}
|
|
12280
|
+
}
|
|
12281
|
+
}
|
|
12282
|
+
/** @type {Map<number, Element[]>} */
|
|
12283
|
+
const pages = new Map();
|
|
12284
|
+
for (const page of wanted) {
|
|
12285
|
+
const answer = answers.find((a) => a.pageRequest.page === page);
|
|
12286
|
+
pages.set(
|
|
12287
|
+
page,
|
|
12288
|
+
window.pages.get(page) ??
|
|
12289
|
+
(answer ? this.#pageRows(answer.pageRequest, filterRequest, answer.response) : []),
|
|
12290
|
+
);
|
|
12291
|
+
}
|
|
12292
|
+
if (kept.length === 0) {
|
|
12293
|
+
this.#body.replaceChildren(...[...pages.values()].flat());
|
|
12294
|
+
} else {
|
|
12295
|
+
this.#body.prepend(...wanted.filter((page) => page < kept[0]).flatMap((page) => pages.get(page) ?? []));
|
|
12296
|
+
this.#body.append(
|
|
12297
|
+
...wanted.filter((page) => page > /** @type number */ (kept.at(-1))).flatMap((page) => pages.get(page) ?? []),
|
|
12298
|
+
);
|
|
12299
|
+
}
|
|
12300
|
+
this.#window = { first, last, pages };
|
|
12301
|
+
this.#total = answers.at(-1)?.response.size ?? this.#total;
|
|
12302
|
+
this.#body.parentElement?.setAttribute('aria-rowcount', String(this.#total + 1));
|
|
12303
|
+
this.#fitSpacers();
|
|
12304
|
+
if (anchor) {
|
|
12305
|
+
this.#wrapper.scrollTop += anchor.getBoundingClientRect().top - anchorTop;
|
|
12306
|
+
}
|
|
12307
|
+
await Rendering.waitForChildren(this.#body).catch(() => undefined);
|
|
12308
|
+
if (claim.stale) {
|
|
12309
|
+
return;
|
|
12310
|
+
}
|
|
12311
|
+
if (anchor) {
|
|
12312
|
+
// the elements in the new rows have rendered since, which can change their height
|
|
12313
|
+
this.#wrapper.scrollTop += anchor.getBoundingClientRect().top - anchorTop;
|
|
12314
|
+
}
|
|
12315
|
+
status.appending.setAttribute('hidden', '');
|
|
12316
|
+
this.#countInView();
|
|
12317
|
+
for (const { pageRequest, response } of answers) {
|
|
12318
|
+
this.dispatchEvent(
|
|
12319
|
+
new CustomEvent('load:success', {
|
|
12320
|
+
bubbles: true,
|
|
12321
|
+
cancelable: false,
|
|
12322
|
+
detail: { pageRequest, sortRequest, filterRequest, response },
|
|
12323
|
+
}),
|
|
12324
|
+
);
|
|
12325
|
+
}
|
|
12326
|
+
} catch (/** @type any */ error) {
|
|
12327
|
+
if (claim.stale) {
|
|
12328
|
+
return;
|
|
12329
|
+
}
|
|
12330
|
+
this.#windowFailed = true;
|
|
12331
|
+
status.appending.setAttribute('hidden', '');
|
|
12332
|
+
status.error.textContent = Localization.of().failure(error);
|
|
12333
|
+
status.failure.removeAttribute('hidden');
|
|
12334
|
+
this.dispatchEvent(
|
|
12335
|
+
new CustomEvent('load:failure', {
|
|
12336
|
+
bubbles: true,
|
|
12337
|
+
cancelable: false,
|
|
12338
|
+
detail: { pageRequest: { page: missing[0], size }, sortRequest, filterRequest, exception: error },
|
|
12339
|
+
}),
|
|
12340
|
+
);
|
|
12341
|
+
return;
|
|
12342
|
+
} finally {
|
|
12343
|
+
if (this.#shifting === claim) {
|
|
12344
|
+
this.#shifting = null;
|
|
12345
|
+
}
|
|
12346
|
+
}
|
|
12347
|
+
this.#reconcile(false);
|
|
12348
|
+
}
|
|
12349
|
+
/**
|
|
12350
|
+
* Asks again for the pages of the window and shows them where they are.
|
|
12351
|
+
* @returns {Promise<void>}
|
|
12352
|
+
*/
|
|
12353
|
+
async #reloadWindow() {
|
|
12354
|
+
const window = /** @type {{ first: number, last: number, pages: Map<number, Element[]> }} */ (this.#window);
|
|
12355
|
+
const status = /** @type TableStatus */ (this.#status);
|
|
12356
|
+
const { sortRequest, filterRequest } = this.#latestRequest;
|
|
12357
|
+
const size = this.#latestRequest.pageRequest.size;
|
|
12358
|
+
this.#requestedFilters = filterRequest;
|
|
12359
|
+
const claim = this.#loads.take();
|
|
12360
|
+
clearTimeout(this.#jump);
|
|
12361
|
+
this.#shifting = null;
|
|
12362
|
+
this.#windowFailed = false;
|
|
12363
|
+
status.appending.setAttribute('hidden', '');
|
|
12364
|
+
status.failure.setAttribute('hidden', '');
|
|
12365
|
+
this.setAttribute('aria-busy', 'true');
|
|
12366
|
+
const wanted = Array.from({ length: window.last - window.first + 1 }, (_, i) => window.first + i);
|
|
12367
|
+
try {
|
|
12368
|
+
const answers = await Promise.all(
|
|
12369
|
+
wanted.map(async (page) => {
|
|
12370
|
+
const pageRequest = { page, size };
|
|
12371
|
+
return { pageRequest, response: await this.#loader.load(pageRequest, sortRequest, filterRequest) };
|
|
12372
|
+
}),
|
|
12373
|
+
);
|
|
12374
|
+
if (claim.stale) {
|
|
12375
|
+
return;
|
|
12376
|
+
}
|
|
12377
|
+
const total = /** @type {{ response: TablePageResponse }} */ (answers.at(-1)).response.size;
|
|
12378
|
+
const pageCount = Math.ceil(total / size);
|
|
12379
|
+
if (!(window.first < pageCount)) {
|
|
12380
|
+
return await this.load({ page: 0, size }, sortRequest, filterRequest);
|
|
12381
|
+
}
|
|
12382
|
+
const shown = answers.filter((a) => a.pageRequest.page < pageCount);
|
|
12383
|
+
const pages = new Map(
|
|
12384
|
+
shown.map((a) => [a.pageRequest.page, this.#pageRows(a.pageRequest, filterRequest, a.response)]),
|
|
12385
|
+
);
|
|
12386
|
+
this.#body.replaceChildren(...[...pages.values()].flat());
|
|
12387
|
+
this.#window = { first: window.first, last: Math.min(window.last, pageCount - 1), pages };
|
|
12388
|
+
this.#total = total;
|
|
12389
|
+
this.#body.parentElement?.setAttribute('aria-rowcount', String(total + 1));
|
|
12390
|
+
this.#fitSpacers();
|
|
12391
|
+
await Rendering.waitForChildren(this.#body).catch(() => undefined);
|
|
12392
|
+
if (claim.stale) {
|
|
12393
|
+
return;
|
|
12394
|
+
}
|
|
12395
|
+
this.removeAttribute('aria-busy');
|
|
12396
|
+
this.#countInView();
|
|
12397
|
+
for (const { pageRequest, response } of shown) {
|
|
12398
|
+
this.dispatchEvent(
|
|
12399
|
+
new CustomEvent('load:success', {
|
|
12400
|
+
bubbles: true,
|
|
12401
|
+
cancelable: false,
|
|
12402
|
+
detail: { pageRequest, sortRequest, filterRequest, response },
|
|
12403
|
+
}),
|
|
12404
|
+
);
|
|
12405
|
+
}
|
|
12406
|
+
this.#reconcile(false);
|
|
12407
|
+
} catch (/** @type any */ error) {
|
|
12408
|
+
if (claim.stale) {
|
|
12409
|
+
return;
|
|
12410
|
+
}
|
|
12411
|
+
this.#fail(error, { pageRequest: { page: window.first, size }, sortRequest, filterRequest });
|
|
12412
|
+
throw error;
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
12415
|
+
#update(pageRequest, sortRequest, filterRequest, pageResponse) {
|
|
12416
|
+
this.#loading.setAttribute('hidden', '');
|
|
12417
|
+
if (this.#virtual) {
|
|
12418
|
+
this.#rowHeight = 0;
|
|
12419
|
+
this.#unlockColumns();
|
|
12420
|
+
}
|
|
12421
|
+
const rows = this.#pageRows(pageRequest, filterRequest, pageResponse);
|
|
12422
|
+
this.#body.replaceChildren(...rows);
|
|
11691
12423
|
this.#empty.toggleAttribute('hidden', pageResponse.data.length !== 0);
|
|
11692
12424
|
if (this.#emptyTemplate && pageResponse.data.length === 0) {
|
|
11693
12425
|
this.#emptyAuthors.replaceChildren(
|
|
@@ -11696,7 +12428,28 @@ var fml = (function (exports) {
|
|
|
11696
12428
|
.render(),
|
|
11697
12429
|
);
|
|
11698
12430
|
}
|
|
11699
|
-
this.#
|
|
12431
|
+
if (this.#virtual) {
|
|
12432
|
+
this.#wrapper.scrollTop = 0;
|
|
12433
|
+
this.#total = pageResponse.size;
|
|
12434
|
+
this.#window =
|
|
12435
|
+
rows.length > 0 ? { first: pageRequest.page, last: pageRequest.page, pages: new Map([[pageRequest.page, rows]]) } : null;
|
|
12436
|
+
return;
|
|
12437
|
+
}
|
|
12438
|
+
if (this.#status) {
|
|
12439
|
+
this.#wrapper.scrollTop = 0;
|
|
12440
|
+
this.#appended = {
|
|
12441
|
+
next: pageRequest.page + 1,
|
|
12442
|
+
shown: pageResponse.data.length,
|
|
12443
|
+
total: pageResponse.size,
|
|
12444
|
+
last: pageResponse.data.length,
|
|
12445
|
+
};
|
|
12446
|
+
this.#status.count.textContent = Localization.of().t('table.loaded', {
|
|
12447
|
+
loaded: this.#appended.shown,
|
|
12448
|
+
total: this.#appended.total,
|
|
12449
|
+
});
|
|
12450
|
+
return;
|
|
12451
|
+
}
|
|
12452
|
+
this.#paginator.update({ current: pageRequest.page, total: Math.ceil(pageResponse.size / pageRequest.size) });
|
|
11700
12453
|
}
|
|
11701
12454
|
}
|
|
11702
12455
|
|
|
@@ -12872,8 +13625,10 @@ var fml = (function (exports) {
|
|
|
12872
13625
|
* failure's problems, one per line, or its message when it carries none.
|
|
12873
13626
|
*/
|
|
12874
13627
|
class SectionRequests {
|
|
12875
|
-
/** @type {WeakSet<Element>} */
|
|
13628
|
+
/** @type {WeakSet<Element>} the sections a first delivery succeeded for */
|
|
12876
13629
|
#entered = new WeakSet();
|
|
13630
|
+
/** @type {WeakSet<Element>} the sections whose first delivery is in flight */
|
|
13631
|
+
#entering = new WeakSet();
|
|
12877
13632
|
/** @type {WeakMap<Element, Claims>} */
|
|
12878
13633
|
#claims = new WeakMap();
|
|
12879
13634
|
|
|
@@ -12882,9 +13637,12 @@ var fml = (function (exports) {
|
|
|
12882
13637
|
* is given, then `section:requested:<name>` when a name is given, each
|
|
12883
13638
|
* bubbling from `host` with the same detail `{ name, section, index, first }`,
|
|
12884
13639
|
* and awaits every answer attached to any of them. `first` is true until a
|
|
12885
|
-
*
|
|
12886
|
-
*
|
|
12887
|
-
*
|
|
13640
|
+
* delivery of the section succeeds: a request nobody listened to does not
|
|
13641
|
+
* spend it, and neither does one whose answers failed, so the next request
|
|
13642
|
+
* is a first one again. While a first delivery is in flight a newer request
|
|
13643
|
+
* for the same section carries `first` false, so the content is not
|
|
13644
|
+
* delivered twice. A request with answers removes the error a previous
|
|
13645
|
+
* failure painted before it waits.
|
|
12888
13646
|
* @param {Element} host the component the events are dispatched on
|
|
12889
13647
|
* @param {Element} section the element the answers deliver into
|
|
12890
13648
|
* @param {string|null} name the section's `data-step`, when it has one
|
|
@@ -12895,7 +13653,7 @@ var fml = (function (exports) {
|
|
|
12895
13653
|
* problems in the section unless a newer request superseded this one
|
|
12896
13654
|
*/
|
|
12897
13655
|
async request(host, section, name, index) {
|
|
12898
|
-
const first = !this.#entered.has(section);
|
|
13656
|
+
const first = !this.#entered.has(section) && !this.#entering.has(section);
|
|
12899
13657
|
const detail = { name, section, index, first };
|
|
12900
13658
|
const types = [
|
|
12901
13659
|
'section:requested',
|
|
@@ -12913,7 +13671,9 @@ var fml = (function (exports) {
|
|
|
12913
13671
|
if (promises.length === 0) {
|
|
12914
13672
|
return undefined;
|
|
12915
13673
|
}
|
|
12916
|
-
|
|
13674
|
+
if (first) {
|
|
13675
|
+
this.#entering.add(section);
|
|
13676
|
+
}
|
|
12917
13677
|
let claims = this.#claims.get(section);
|
|
12918
13678
|
if (!claims) {
|
|
12919
13679
|
claims = new Claims();
|
|
@@ -12929,13 +13689,20 @@ var fml = (function (exports) {
|
|
|
12929
13689
|
}
|
|
12930
13690
|
});
|
|
12931
13691
|
try {
|
|
12932
|
-
|
|
13692
|
+
const answers = await Promise.all(promises);
|
|
13693
|
+
if (first) {
|
|
13694
|
+
this.#entered.add(section);
|
|
13695
|
+
}
|
|
13696
|
+
return answers;
|
|
12933
13697
|
} catch (cause) {
|
|
12934
13698
|
if (owned()) {
|
|
12935
13699
|
this.#paintError(section, cause);
|
|
12936
13700
|
}
|
|
12937
13701
|
throw cause;
|
|
12938
13702
|
} finally {
|
|
13703
|
+
if (first) {
|
|
13704
|
+
this.#entering.delete(section);
|
|
13705
|
+
}
|
|
12939
13706
|
cancelAnimationFrame(frame);
|
|
12940
13707
|
if (owned()) {
|
|
12941
13708
|
section.toggleAttribute('loading', false);
|
|
@@ -13052,15 +13819,27 @@ var fml = (function (exports) {
|
|
|
13052
13819
|
/**
|
|
13053
13820
|
* Calls `dismiss` when a press and its release both land on the backdrop,
|
|
13054
13821
|
* so a selection dragged out of the panel and released outside does not.
|
|
13822
|
+
* The backdrop is where the pointer is outside the dialog's rectangle: a
|
|
13823
|
+
* part of the panel no child covers, the room around the error section
|
|
13824
|
+
* for one, is the panel.
|
|
13055
13825
|
* @param {() => void} dismiss
|
|
13056
13826
|
*/
|
|
13057
13827
|
onBackdrop(dismiss) {
|
|
13828
|
+
const onBackdrop = (/** @type MouseEvent */ e) => {
|
|
13829
|
+
if (e.target !== this.#dialog) {
|
|
13830
|
+
return false;
|
|
13831
|
+
}
|
|
13832
|
+
const panel = this.#dialog.getBoundingClientRect();
|
|
13833
|
+
return (
|
|
13834
|
+
e.clientX < panel.left || e.clientX > panel.right || e.clientY < panel.top || e.clientY > panel.bottom
|
|
13835
|
+
);
|
|
13836
|
+
};
|
|
13058
13837
|
let pressedOutside = false;
|
|
13059
13838
|
this.#dialog.addEventListener('mousedown', (e) => {
|
|
13060
|
-
pressedOutside = e
|
|
13839
|
+
pressedOutside = onBackdrop(e);
|
|
13061
13840
|
});
|
|
13062
13841
|
this.#dialog.addEventListener('click', (e) => {
|
|
13063
|
-
if (pressedOutside && e
|
|
13842
|
+
if (pressedOutside && onBackdrop(e)) {
|
|
13064
13843
|
dismiss();
|
|
13065
13844
|
}
|
|
13066
13845
|
});
|
|
@@ -13337,7 +14116,7 @@ var fml = (function (exports) {
|
|
|
13337
14116
|
* opening, hides the error and loading sections, and dispatches
|
|
13338
14117
|
* `section:requested` on the element (bubbling, with
|
|
13339
14118
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
13340
|
-
* the body and `first` true
|
|
14119
|
+
* the body and `first` true until a delivery succeeds), so a
|
|
13341
14120
|
* listener can fill the body. A failed request is painted into the body
|
|
13342
14121
|
* and does not reject. On a dialog already open nothing is shown or
|
|
13343
14122
|
* requested, and the call waits for the same ending as the calls before it;
|
|
@@ -13589,11 +14368,7 @@ var fml = (function (exports) {
|
|
|
13589
14368
|
#answer = null;
|
|
13590
14369
|
#resolvers = [];
|
|
13591
14370
|
#closing = false;
|
|
13592
|
-
#
|
|
13593
|
-
if (e.target === this.#dialog) {
|
|
13594
|
-
this.#closeNow();
|
|
13595
|
-
}
|
|
13596
|
-
};
|
|
14371
|
+
#slides = new Claims();
|
|
13597
14372
|
/**
|
|
13598
14373
|
* @param {{ slots: Record<string, DocumentFragment> }} c
|
|
13599
14374
|
*/
|
|
@@ -13720,7 +14495,7 @@ var fml = (function (exports) {
|
|
|
13720
14495
|
* opening, hides the error and loading sections, and dispatches
|
|
13721
14496
|
* `section:requested` on the element (bubbling, with
|
|
13722
14497
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
13723
|
-
* the content section and `first` true
|
|
14498
|
+
* the content section and `first` true until a delivery succeeds),
|
|
13724
14499
|
* so a listener can fill the content. A failed request is painted into the
|
|
13725
14500
|
* content section and does not reject. On a drawer already open nothing is
|
|
13726
14501
|
* shown or requested, except that one sliding out stops and stays open,
|
|
@@ -13771,11 +14546,13 @@ var fml = (function (exports) {
|
|
|
13771
14546
|
/**
|
|
13772
14547
|
* Slides the drawer out and closes it when the animation ends, setting the
|
|
13773
14548
|
* `closing` attribute on the native dialog meanwhile; the `close` event is
|
|
13774
|
-
* dispatched then, after this call has returned.
|
|
13775
|
-
*
|
|
13776
|
-
*
|
|
13777
|
-
*
|
|
13778
|
-
*
|
|
14549
|
+
* dispatched then, after this call has returned. An animation the page
|
|
14550
|
+
* cancels, by hiding the drawer or an ancestor of it, closes it as one
|
|
14551
|
+
* that ended does. Where the user prefers reduced motion, or no animation
|
|
14552
|
+
* runs, it closes at once. A non-empty result answers as the button
|
|
14553
|
+
* carrying that `data-result` would; no result, or an empty one, is a
|
|
14554
|
+
* dismissal. Does nothing on a drawer that is closed or already sliding
|
|
14555
|
+
* out.
|
|
13779
14556
|
* @param {string} [result]
|
|
13780
14557
|
*/
|
|
13781
14558
|
close(result) {
|
|
@@ -13790,12 +14567,18 @@ var fml = (function (exports) {
|
|
|
13790
14567
|
return;
|
|
13791
14568
|
}
|
|
13792
14569
|
this.#dialog.setAttribute('closing', '');
|
|
13793
|
-
|
|
14570
|
+
const animations = this.#dialog.getAnimations();
|
|
14571
|
+
if (animations.length === 0) {
|
|
13794
14572
|
this.#closeNow();
|
|
13795
14573
|
return;
|
|
13796
14574
|
}
|
|
13797
14575
|
this.#closing = true;
|
|
13798
|
-
this.#
|
|
14576
|
+
const slide = this.#slides.take();
|
|
14577
|
+
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
|
14578
|
+
if (!slide.stale) {
|
|
14579
|
+
this.#closeNow();
|
|
14580
|
+
}
|
|
14581
|
+
});
|
|
13799
14582
|
}
|
|
13800
14583
|
#closeNow() {
|
|
13801
14584
|
this.#stopSlidingOut();
|
|
@@ -13803,7 +14586,7 @@ var fml = (function (exports) {
|
|
|
13803
14586
|
}
|
|
13804
14587
|
#stopSlidingOut() {
|
|
13805
14588
|
this.#closing = false;
|
|
13806
|
-
this.#
|
|
14589
|
+
this.#slides.invalidate();
|
|
13807
14590
|
this.#dialog.removeAttribute('closing');
|
|
13808
14591
|
}
|
|
13809
14592
|
/** @returns {boolean} whether this call is the one that opened the modal */
|
|
@@ -14348,7 +15131,7 @@ var fml = (function (exports) {
|
|
|
14348
15131
|
}
|
|
14349
15132
|
/**
|
|
14350
15133
|
* Fires the section requests again for a panel, whether it is visible or
|
|
14351
|
-
* not, with `first` false once an earlier
|
|
15134
|
+
* not, with `first` false once an earlier delivery succeeded.
|
|
14352
15135
|
* @param {number | Element} ref the panel's index, or the panel itself
|
|
14353
15136
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
14354
15137
|
* the answers, or to undefined when nobody listened or the delivery
|
|
@@ -14571,7 +15354,7 @@ var fml = (function (exports) {
|
|
|
14571
15354
|
}
|
|
14572
15355
|
/**
|
|
14573
15356
|
* Fires the section requests again for a section, whether it is current or
|
|
14574
|
-
* not, with `first` false once an earlier
|
|
15357
|
+
* not, with `first` false once an earlier delivery succeeded.
|
|
14575
15358
|
* @param {string | Element} ref the section's `data-step`, or the section
|
|
14576
15359
|
* itself
|
|
14577
15360
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
@@ -14633,6 +15416,9 @@ var fml = (function (exports) {
|
|
|
14633
15416
|
'table.initial': 'Start searching to see results.',
|
|
14634
15417
|
'table.error': 'Error while loading data:',
|
|
14635
15418
|
'table.no-data': 'No elements found.',
|
|
15419
|
+
'table.loaded': 'Showing {loaded} of {total}',
|
|
15420
|
+
'table.range': 'Rows {from}-{to} of {total}',
|
|
15421
|
+
'table.retry': 'Retry',
|
|
14636
15422
|
'dropdown.empty': 'No results',
|
|
14637
15423
|
'select.remove': 'Remove',
|
|
14638
15424
|
'files.dropzone-label': 'Click or drop your files here',
|
|
@@ -14684,6 +15470,9 @@ var fml = (function (exports) {
|
|
|
14684
15470
|
'table.initial': 'Avvia la ricerca per visualizzare i risultati.',
|
|
14685
15471
|
'table.error': 'Errore nel caricamento dei dati:',
|
|
14686
15472
|
'table.no-data': 'Nessun elemento trovato.',
|
|
15473
|
+
'table.loaded': 'Mostrati {loaded} di {total}',
|
|
15474
|
+
'table.range': 'Righe {from}-{to} di {total}',
|
|
15475
|
+
'table.retry': 'Riprova',
|
|
14687
15476
|
'dropdown.empty': 'Nessun risultato',
|
|
14688
15477
|
'select.remove': 'Rimuovi',
|
|
14689
15478
|
'files.dropzone-label': 'Clicca o trascina i file qui',
|
|
@@ -14735,6 +15524,9 @@ var fml = (function (exports) {
|
|
|
14735
15524
|
'table.initial': 'Inicia la búsqueda para ver los resultados.',
|
|
14736
15525
|
'table.error': 'Error al cargar los datos:',
|
|
14737
15526
|
'table.no-data': 'No se encontraron elementos.',
|
|
15527
|
+
'table.loaded': 'Mostrando {loaded} de {total}',
|
|
15528
|
+
'table.range': 'Filas {from}-{to} de {total}',
|
|
15529
|
+
'table.retry': 'Reintentar',
|
|
14738
15530
|
'dropdown.empty': 'Sin resultados',
|
|
14739
15531
|
'select.remove': 'Eliminar',
|
|
14740
15532
|
'files.dropzone-label': 'Haz clic o arrastra tus archivos aquí',
|
|
@@ -14789,6 +15581,9 @@ var fml = (function (exports) {
|
|
|
14789
15581
|
'table.initial': 'Lancez la recherche pour voir les résultats.',
|
|
14790
15582
|
'table.error': 'Erreur lors du chargement des données :',
|
|
14791
15583
|
'table.no-data': 'Aucun élément trouvé.',
|
|
15584
|
+
'table.loaded': 'Affichage de {loaded} sur {total}',
|
|
15585
|
+
'table.range': 'Lignes {from} à {to} sur {total}',
|
|
15586
|
+
'table.retry': 'Réessayer',
|
|
14792
15587
|
'dropdown.empty': 'Aucun résultat',
|
|
14793
15588
|
'select.remove': 'Retirer',
|
|
14794
15589
|
'files.dropzone-label': 'Cliquez ou déposez vos fichiers ici',
|
|
@@ -14850,8 +15645,11 @@ var fml = (function (exports) {
|
|
|
14850
15645
|
/**
|
|
14851
15646
|
* @param {{ language?: string, translations?: Record<string, any>, httpClient?: HttpClient }} [options]
|
|
14852
15647
|
* `language` is fixed for the page: a full BCP-47 tag or a primary subtag,
|
|
14853
|
-
* defaulting to `navigator.language`, then to 'en'
|
|
14854
|
-
*
|
|
15648
|
+
* defaulting to `navigator.language`, then to 'en', an empty one included.
|
|
15649
|
+
* It is canonicalized (`IT-it` and `it_IT` both read `it-IT`), and one that
|
|
15650
|
+
* is no language tag at all throws here, rather than at the first message
|
|
15651
|
+
* or number formatted in it. Its primary subtag picks the built-in
|
|
15652
|
+
* translations (en, it, es, fr); the messages
|
|
14855
15653
|
* are the english ones, overridden by that language's, overridden by
|
|
14856
15654
|
* `translations`, so a language without built-in translations reads in
|
|
14857
15655
|
* english. `translations` is a flat map of keys to messages: reword built-in
|
|
@@ -14859,9 +15657,18 @@ var fml = (function (exports) {
|
|
|
14859
15657
|
* `httpClient` is the client every ful component fetches through, registered
|
|
14860
15658
|
* as the `http-client` component; without one, `configure` builds a client
|
|
14861
15659
|
* that sends the csrf token and navigates to '/' on a 401 response.
|
|
15660
|
+
* @throws {RangeError} when `language` is not a language tag
|
|
14862
15661
|
*/
|
|
14863
15662
|
constructor(options = {}) {
|
|
14864
|
-
|
|
15663
|
+
const language = options.language || navigator?.language || 'en';
|
|
15664
|
+
try {
|
|
15665
|
+
// unicode locale identifiers separate subtags with an underscore too, Intl takes the dash alone
|
|
15666
|
+
this.#language = Intl.getCanonicalLocales(String(language).replaceAll('_', '-'))[0];
|
|
15667
|
+
} catch {
|
|
15668
|
+
throw new RangeError(
|
|
15669
|
+
`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`,
|
|
15670
|
+
);
|
|
15671
|
+
}
|
|
14865
15672
|
this.#translations = options.translations ?? {};
|
|
14866
15673
|
this.#httpClient = options.httpClient ?? null;
|
|
14867
15674
|
}
|
|
@@ -14870,7 +15677,7 @@ var fml = (function (exports) {
|
|
|
14870
15677
|
* Defines on `registry` the `l10n` module, the `http-client` component,
|
|
14871
15678
|
* every `ful-*` element, the `loaders:select`, `loaders:form` and
|
|
14872
15679
|
* `loaders:table` components, and an overlay publishing `l10n` (the merged
|
|
14873
|
-
* messages) and `locale` (the `language
|
|
15680
|
+
* messages) and `locale` (the `language`, canonicalized) to every template.
|
|
14874
15681
|
* @param {{ defineModule(name: string, module: any): any, defineComponent(name: string, component: any): any, defineElement(tag: string, klass: CustomElementConstructor): any, defineOverlay(...data: any[]): any }} registry
|
|
14875
15682
|
* the ftl `Registry`, each of those methods answering the registry itself
|
|
14876
15683
|
* @returns {void}
|
|
@@ -14878,7 +15685,7 @@ var fml = (function (exports) {
|
|
|
14878
15685
|
configure(registry) {
|
|
14879
15686
|
const httpClient =
|
|
14880
15687
|
this.#httpClient ?? HttpClient.builder().withCsrfToken().withRedirectOnUnauthorized('/').build();
|
|
14881
|
-
const language = this.#language.split('-')[0]
|
|
15688
|
+
const language = this.#language.split('-')[0];
|
|
14882
15689
|
const l10n = { ...BUILTIN.en, ...BUILTIN[language], ...this.#translations };
|
|
14883
15690
|
registry
|
|
14884
15691
|
.defineModule('l10n', Localization)
|
|
@@ -14961,7 +15768,6 @@ var fml = (function (exports) {
|
|
|
14961
15768
|
SortButton: SortButton,
|
|
14962
15769
|
Table: Table,
|
|
14963
15770
|
TableLoader: TableLoader,
|
|
14964
|
-
TableSchemaParser: TableSchemaParser,
|
|
14965
15771
|
Tabs: Tabs,
|
|
14966
15772
|
TextFilter: TextFilter,
|
|
14967
15773
|
Timing: Timing,
|
|
@@ -15040,7 +15846,6 @@ var fml = (function (exports) {
|
|
|
15040
15846
|
exports.SortButton = SortButton;
|
|
15041
15847
|
exports.Table = Table;
|
|
15042
15848
|
exports.TableLoader = TableLoader;
|
|
15043
|
-
exports.TableSchemaParser = TableSchemaParser;
|
|
15044
15849
|
exports.Tabs = Tabs;
|
|
15045
15850
|
exports.Template = Template;
|
|
15046
15851
|
exports.Templates = Templates;
|