@optionfactory/fml 8.0.0 → 8.0.1
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/client-errors.iife.js +11 -8
- package/dist/client-errors.iife.js.map +1 -1
- package/dist/client-errors.iife.min.js +1 -1
- package/dist/client-errors.iife.min.js.map +1 -1
- package/dist/fml.css +10 -1
- package/dist/fml.css.map +1 -1
- package/dist/fml.d.mts +73 -13
- package/dist/fml.iife.js +598 -208
- 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/fml.min.mjs +1 -1
- package/dist/fml.min.mjs.map +1 -1
- package/dist/fml.mjs +598 -208
- package/dist/fml.mjs.map +1 -1
- package/dist/ftl.d.mts +2 -1
- package/dist/ftl.iife.js +60 -33
- 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 +60 -33
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +10 -1
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +22 -6
- package/dist/ful.iife.js +496 -166
- 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 +496 -166
- package/dist/ful.mjs.map +1 -1
- package/dist/httpc.d.mts +49 -6
- package/dist/httpc.iife.js +40 -9
- package/dist/httpc.iife.js.map +1 -1
- package/dist/httpc.iife.min.js +1 -1
- package/dist/httpc.iife.min.js.map +1 -1
- package/dist/httpc.min.mjs +1 -1
- package/dist/httpc.min.mjs.map +1 -1
- package/dist/httpc.mjs +40 -9
- package/dist/httpc.mjs.map +1 -1
- package/package.json +6 -7
package/dist/fml.mjs
CHANGED
|
@@ -3457,7 +3457,7 @@ class EvaluatingVisitor {
|
|
|
3457
3457
|
case '<=':
|
|
3458
3458
|
return lhs <= rhs;
|
|
3459
3459
|
default:
|
|
3460
|
-
throw new Error(
|
|
3460
|
+
throw new Error(`unknown cmp op ${node.op}`);
|
|
3461
3461
|
}
|
|
3462
3462
|
}
|
|
3463
3463
|
[nodes.call](node) {
|
|
@@ -3468,7 +3468,7 @@ class EvaluatingVisitor {
|
|
|
3468
3468
|
}
|
|
3469
3469
|
const fn = module[fnRef.value];
|
|
3470
3470
|
if (!fn) {
|
|
3471
|
-
throw new Error(`Function "#${fnRef.module === null ? '' : fnRef.module
|
|
3471
|
+
throw new Error(`Function "#${fnRef.module === null ? '' : `${fnRef.module}:`}${fnRef.value}" not found`);
|
|
3472
3472
|
}
|
|
3473
3473
|
const args = node.args.map((arg) => this.visit(arg));
|
|
3474
3474
|
return fn.apply(this.#resolve_proxy(), args);
|
|
@@ -3504,14 +3504,14 @@ class EvaluatingVisitor {
|
|
|
3504
3504
|
return cond ? cond : this.visit(node.ifFalse);
|
|
3505
3505
|
}
|
|
3506
3506
|
[nodes.access](node) {
|
|
3507
|
-
let prev
|
|
3507
|
+
let prev ;
|
|
3508
3508
|
let cur = this.visit(node.lhs);
|
|
3509
3509
|
for (let i = 0; i !== node.rhs.length; ++i) {
|
|
3510
3510
|
const rhs = node.rhs[i];
|
|
3511
3511
|
if (rhs.ns && cur == null) {
|
|
3512
3512
|
return undefined;
|
|
3513
3513
|
}
|
|
3514
|
-
let value
|
|
3514
|
+
let value ;
|
|
3515
3515
|
switch (rhs.type) {
|
|
3516
3516
|
case nodes.member: {
|
|
3517
3517
|
value = cur[rhs.rhs];
|
|
@@ -3552,7 +3552,7 @@ class EvaluatingVisitor {
|
|
|
3552
3552
|
case nodes.templated.ten:
|
|
3553
3553
|
return { type: nodes.dom.n, value: this.visit(node.value) };
|
|
3554
3554
|
default:
|
|
3555
|
-
throw new Error(
|
|
3555
|
+
throw new Error(`unknown node type ${node.type.toString()}`);
|
|
3556
3556
|
}
|
|
3557
3557
|
});
|
|
3558
3558
|
}
|
|
@@ -3728,7 +3728,7 @@ class Attributes {
|
|
|
3728
3728
|
.forEach((a) => {
|
|
3729
3729
|
const target = a.substring(prefix.length);
|
|
3730
3730
|
if (target === 'class') {
|
|
3731
|
-
const classes = from.getAttribute(prefix
|
|
3731
|
+
const classes = from.getAttribute(`${prefix}class`)?.split(/\s+/).filter((a) => a.length) ?? [];
|
|
3732
3732
|
to.classList.add(...classes);
|
|
3733
3733
|
return;
|
|
3734
3734
|
}
|
|
@@ -3820,8 +3820,7 @@ class Nodes {
|
|
|
3820
3820
|
* @returns
|
|
3821
3821
|
*/
|
|
3822
3822
|
static isParsed(el) {
|
|
3823
|
-
|
|
3824
|
-
for (let c = el; c; c = c.parentNode) {
|
|
3823
|
+
for (let c = /** @type {Node | null} */ (el); c; c = c.parentNode) {
|
|
3825
3824
|
if (c.nextSibling) {
|
|
3826
3825
|
return true;
|
|
3827
3826
|
}
|
|
@@ -3884,19 +3883,18 @@ class Nodes {
|
|
|
3884
3883
|
}
|
|
3885
3884
|
|
|
3886
3885
|
class NodeOperations {
|
|
3887
|
-
#forRemoval;
|
|
3888
|
-
constructor() {
|
|
3889
|
-
this.#forRemoval = [];
|
|
3890
|
-
}
|
|
3886
|
+
#forRemoval = new Set();
|
|
3891
3887
|
removed(node) {
|
|
3892
|
-
return this.#forRemoval.
|
|
3888
|
+
return this.#forRemoval.has(node);
|
|
3893
3889
|
}
|
|
3894
3890
|
remove(node) {
|
|
3895
3891
|
node.replaceChildren?.();
|
|
3896
3892
|
Object.keys(node.dataset || {})
|
|
3897
3893
|
.filter((k) => k.startsWith('tpl'))
|
|
3898
|
-
.forEach((k) =>
|
|
3899
|
-
|
|
3894
|
+
.forEach((k) => {
|
|
3895
|
+
delete node.dataset[k];
|
|
3896
|
+
});
|
|
3897
|
+
this.#forRemoval.add(node);
|
|
3900
3898
|
}
|
|
3901
3899
|
popData(node, key) {
|
|
3902
3900
|
const v = node.dataset[key];
|
|
@@ -3914,9 +3912,10 @@ class NodeOperations {
|
|
|
3914
3912
|
this.remove(ref);
|
|
3915
3913
|
}
|
|
3916
3914
|
cleanup() {
|
|
3917
|
-
|
|
3918
|
-
|
|
3915
|
+
for (const node of this.#forRemoval) {
|
|
3916
|
+
node.remove();
|
|
3919
3917
|
}
|
|
3918
|
+
this.#forRemoval.clear();
|
|
3920
3919
|
}
|
|
3921
3920
|
}
|
|
3922
3921
|
|
|
@@ -3971,7 +3970,7 @@ class CommandsHandler {
|
|
|
3971
3970
|
}
|
|
3972
3971
|
static tplRemove(node, value, ops, modules, dataStack) {
|
|
3973
3972
|
switch (value.toLowerCase()) {
|
|
3974
|
-
case 'tag':
|
|
3973
|
+
case 'tag': {
|
|
3975
3974
|
const fragment = new DocumentFragment();
|
|
3976
3975
|
while (node.firstChild) {
|
|
3977
3976
|
fragment.appendChild(node.firstChild);
|
|
@@ -3985,6 +3984,7 @@ class CommandsHandler {
|
|
|
3985
3984
|
ops.remove(node);
|
|
3986
3985
|
}
|
|
3987
3986
|
break;
|
|
3987
|
+
}
|
|
3988
3988
|
case 'body':
|
|
3989
3989
|
node.replaceChildren();
|
|
3990
3990
|
break;
|
|
@@ -4050,8 +4050,10 @@ class CommandsHandler {
|
|
|
4050
4050
|
}
|
|
4051
4051
|
}
|
|
4052
4052
|
|
|
4053
|
-
// Module-isolated string cache for dataset-to-attribute conversions
|
|
4053
|
+
// Module-isolated string cache for dataset-to-attribute conversions, bounded with
|
|
4054
|
+
// FIFO eviction like the expression ast cache
|
|
4054
4055
|
const attributeCache = new Map();
|
|
4056
|
+
const ATTRIBUTE_CACHE_MAX_SIZE = 1000;
|
|
4055
4057
|
|
|
4056
4058
|
/**
|
|
4057
4059
|
* Converts a tpl camelCase dataset key into a kebab-case attribute name.
|
|
@@ -4062,6 +4064,9 @@ const attributeCache = new Map();
|
|
|
4062
4064
|
function toAttr(dataSetKey) {
|
|
4063
4065
|
let cached = attributeCache.get(dataSetKey);
|
|
4064
4066
|
if (!cached) {
|
|
4067
|
+
if (attributeCache.size >= ATTRIBUTE_CACHE_MAX_SIZE) {
|
|
4068
|
+
attributeCache.delete(attributeCache.keys().next().value);
|
|
4069
|
+
}
|
|
4065
4070
|
cached = dataSetKey
|
|
4066
4071
|
.substring(3)
|
|
4067
4072
|
.split(/(?=[A-Z])/)
|
|
@@ -4196,9 +4201,10 @@ class Template {
|
|
|
4196
4201
|
* @param {string} expression
|
|
4197
4202
|
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode]
|
|
4198
4203
|
* @param {...*} data
|
|
4204
|
+
* @returns the evaluated expression result
|
|
4199
4205
|
*/
|
|
4200
4206
|
evaluate(expression, mode, ...data) {
|
|
4201
|
-
new ExpressionEvaluator(this.#modules, this.#dataStack).withOverlay(...data).evaluate(expression, mode);
|
|
4207
|
+
return new ExpressionEvaluator(this.#modules, this.#dataStack).withOverlay(...data).evaluate(expression, mode);
|
|
4202
4208
|
}
|
|
4203
4209
|
/**
|
|
4204
4210
|
* Returns an expression evaluator with bound modules and dataStack.
|
|
@@ -4343,8 +4349,8 @@ class RenderError extends Error {
|
|
|
4343
4349
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
4344
4350
|
node.nodeValue = node.nodeValue.trim();
|
|
4345
4351
|
}
|
|
4346
|
-
for (
|
|
4347
|
-
|
|
4352
|
+
for (let n = 0; n < node.childNodes.length; n++) {
|
|
4353
|
+
const child = node.childNodes[n];
|
|
4348
4354
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
4349
4355
|
child.nodeValue = child.nodeValue.trim();
|
|
4350
4356
|
if (child.nodeValue.length === 0) {
|
|
@@ -4363,8 +4369,7 @@ class UpgradeQueue {
|
|
|
4363
4369
|
#q = new Map();
|
|
4364
4370
|
constructor() {
|
|
4365
4371
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
4366
|
-
|
|
4367
|
-
await Promise.all(pending);
|
|
4372
|
+
await this.settle();
|
|
4368
4373
|
document.dispatchEvent(
|
|
4369
4374
|
new CustomEvent('ftl:ready', {
|
|
4370
4375
|
bubbles: false,
|
|
@@ -4384,6 +4389,16 @@ class UpgradeQueue {
|
|
|
4384
4389
|
.finally(() => this.#q.delete(el));
|
|
4385
4390
|
this.#q.set(el, promise);
|
|
4386
4391
|
}
|
|
4392
|
+
/**
|
|
4393
|
+
* Waits for every queued upgrade, including the ones enqueued while waiting:
|
|
4394
|
+
* a component is only queued once its parent connects it, so a single pass would
|
|
4395
|
+
* miss everything nested.
|
|
4396
|
+
*/
|
|
4397
|
+
async settle() {
|
|
4398
|
+
while (this.#q.size !== 0) {
|
|
4399
|
+
await Promise.all(Array.from(this.#q.values()));
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4387
4402
|
get entries() {
|
|
4388
4403
|
return this.#q.entries();
|
|
4389
4404
|
}
|
|
@@ -4578,18 +4593,30 @@ class Templates {
|
|
|
4578
4593
|
}
|
|
4579
4594
|
}
|
|
4580
4595
|
|
|
4581
|
-
|
|
4582
|
-
|
|
4596
|
+
/**
|
|
4597
|
+
* Waits for the queued upgrades matching the filter, including the ones enqueued while
|
|
4598
|
+
* waiting: a component is only queued once its parent connects it, so a single pass
|
|
4599
|
+
* would miss everything nested.
|
|
4600
|
+
* @param {(el: Element) => boolean} accept
|
|
4601
|
+
*/
|
|
4602
|
+
const settle = async (accept) => {
|
|
4603
|
+
for (;;) {
|
|
4583
4604
|
const pending = Array.from(registry.upgrades)
|
|
4584
|
-
.filter(([child
|
|
4585
|
-
.map(([
|
|
4605
|
+
.filter(([child]) => accept(child))
|
|
4606
|
+
.map(([, promise]) => promise);
|
|
4607
|
+
if (pending.length === 0) {
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4586
4610
|
await Promise.all(pending);
|
|
4587
4611
|
}
|
|
4612
|
+
};
|
|
4613
|
+
|
|
4614
|
+
class Rendering {
|
|
4615
|
+
static async waitFor(el) {
|
|
4616
|
+
await settle((child) => el.contains(child));
|
|
4617
|
+
}
|
|
4588
4618
|
static async waitForChildren(el) {
|
|
4589
|
-
|
|
4590
|
-
.filter(([child, promise]) => el !== child && el.contains(child))
|
|
4591
|
-
.map(([child, promise]) => promise);
|
|
4592
|
-
await Promise.all(pending);
|
|
4619
|
+
await settle((child) => el !== child && el.contains(child));
|
|
4593
4620
|
}
|
|
4594
4621
|
}
|
|
4595
4622
|
|
|
@@ -4631,7 +4658,7 @@ class ParsedElement extends HTMLElement {
|
|
|
4631
4658
|
template(name) {
|
|
4632
4659
|
const { modules, data } = registry.context();
|
|
4633
4660
|
let t = this.#bits().TEMPLATES[name ?? 'default'].withData(data).withModules(modules);
|
|
4634
|
-
for (
|
|
4661
|
+
for (const k of ['l10n', 'config']) {
|
|
4635
4662
|
const v = this.constructor[k];
|
|
4636
4663
|
if (v) {
|
|
4637
4664
|
t = t.withOverlay({ [k]: v });
|
|
@@ -4772,13 +4799,22 @@ class Base64 {
|
|
|
4772
4799
|
|
|
4773
4800
|
let vi = 0;
|
|
4774
4801
|
let si = 0;
|
|
4775
|
-
|
|
4802
|
+
//every group yields three bytes, but a padded input stops short of the last
|
|
4803
|
+
//one or two: writing them anyway would rely on typed arrays dropping writes
|
|
4804
|
+
//past their length
|
|
4805
|
+
while (vi < nbytes) {
|
|
4776
4806
|
const v1 = d.indexOf(str.charAt(si++));
|
|
4777
4807
|
const v2 = d.indexOf(str.charAt(si++));
|
|
4778
4808
|
const v3 = d.indexOf(str.charAt(si++));
|
|
4779
4809
|
const v4 = d.indexOf(str.charAt(si++));
|
|
4780
4810
|
view[vi++] = (v1 << 2) | (v2 >> 4);
|
|
4811
|
+
if (vi === nbytes) {
|
|
4812
|
+
break;
|
|
4813
|
+
}
|
|
4781
4814
|
view[vi++] = ((v2 & 15) << 4) | (v3 >> 2);
|
|
4815
|
+
if (vi === nbytes) {
|
|
4816
|
+
break;
|
|
4817
|
+
}
|
|
4782
4818
|
view[vi++] = ((v3 & 3) << 6) | v4;
|
|
4783
4819
|
}
|
|
4784
4820
|
|
|
@@ -4805,7 +4841,7 @@ class Hex {
|
|
|
4805
4841
|
return Array.from(bytes)
|
|
4806
4842
|
.map((b) => b.toString(16))
|
|
4807
4843
|
.map((b) => (upper ? b.toUpperCase() : b))
|
|
4808
|
-
.map((o) => o.padStart(2, 0))
|
|
4844
|
+
.map((o) => o.padStart(2, '0'))
|
|
4809
4845
|
.join('');
|
|
4810
4846
|
}
|
|
4811
4847
|
}
|
|
@@ -5135,6 +5171,26 @@ const unmarshal = async (response, type) => {
|
|
|
5135
5171
|
}
|
|
5136
5172
|
};
|
|
5137
5173
|
|
|
5174
|
+
/**
|
|
5175
|
+
* Yields the entries of a headers or params initializer preserving nullish values:
|
|
5176
|
+
* normalizing through `Headers`/`URLSearchParams` first would stringify them to
|
|
5177
|
+
* "null"/"undefined" instead of removing the key.
|
|
5178
|
+
* @param {any} source
|
|
5179
|
+
* @returns {Iterable<[string, any]>}
|
|
5180
|
+
*/
|
|
5181
|
+
const rawEntries = (source) => {
|
|
5182
|
+
if (source == null) {
|
|
5183
|
+
return [];
|
|
5184
|
+
}
|
|
5185
|
+
if (typeof source === 'string') {
|
|
5186
|
+
return new URLSearchParams(source);
|
|
5187
|
+
}
|
|
5188
|
+
if (typeof source[Symbol.iterator] === 'function') {
|
|
5189
|
+
return source;
|
|
5190
|
+
}
|
|
5191
|
+
return Object.entries(source);
|
|
5192
|
+
};
|
|
5193
|
+
|
|
5138
5194
|
class HttpRequestBuilder {
|
|
5139
5195
|
#client;
|
|
5140
5196
|
#method;
|
|
@@ -5187,11 +5243,11 @@ class HttpRequestBuilder {
|
|
|
5187
5243
|
}
|
|
5188
5244
|
/**
|
|
5189
5245
|
* Add all passed headers to the request, overriding existing ones if that key already exists. Null and undefined values cause the key to be removed.
|
|
5190
|
-
* @param {HeadersInit} hs
|
|
5246
|
+
* @param {HeadersInit|Record<string,string|null|undefined>} hs
|
|
5191
5247
|
* @returns {HttpRequestBuilder} this builder
|
|
5192
5248
|
*/
|
|
5193
5249
|
headers(hs) {
|
|
5194
|
-
for (const [k, v] of
|
|
5250
|
+
for (const [k, v] of rawEntries(hs)) {
|
|
5195
5251
|
if (v == null) {
|
|
5196
5252
|
this.#headers.delete(k);
|
|
5197
5253
|
} else {
|
|
@@ -5216,11 +5272,11 @@ class HttpRequestBuilder {
|
|
|
5216
5272
|
}
|
|
5217
5273
|
/**
|
|
5218
5274
|
* Add all query parameters to the request, overriding existing ones if that key already exists. Null and undefined values cause the key to be removed
|
|
5219
|
-
* @param {URLSearchParams|Record<string,string>|string[][]|string} ps
|
|
5275
|
+
* @param {URLSearchParams|Record<string,string|null|undefined>|string[][]|string} ps
|
|
5220
5276
|
* @returns {HttpRequestBuilder} this builder
|
|
5221
5277
|
*/
|
|
5222
5278
|
params(ps) {
|
|
5223
|
-
for (const [k, v] of
|
|
5279
|
+
for (const [k, v] of rawEntries(ps)) {
|
|
5224
5280
|
if (v == null) {
|
|
5225
5281
|
this.#params.delete(k);
|
|
5226
5282
|
} else {
|
|
@@ -5236,8 +5292,10 @@ class HttpRequestBuilder {
|
|
|
5236
5292
|
* @returns {HttpRequestBuilder} this builder
|
|
5237
5293
|
*/
|
|
5238
5294
|
param(k, ...vs) {
|
|
5295
|
+
//overriding, as header, headers and params all do: pass every value in one
|
|
5296
|
+
//call to get a multi valued parameter
|
|
5297
|
+
this.#params.delete(k);
|
|
5239
5298
|
if (vs.length === 0 || vs[0] == null) {
|
|
5240
|
-
this.#params.delete(k);
|
|
5241
5299
|
return this;
|
|
5242
5300
|
}
|
|
5243
5301
|
for (const v of vs) {
|
|
@@ -5270,7 +5328,7 @@ class HttpRequestBuilder {
|
|
|
5270
5328
|
/**
|
|
5271
5329
|
* Sets the request body as a FormData configured using the callback.
|
|
5272
5330
|
* `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
|
|
5273
|
-
* @param {
|
|
5331
|
+
* @param {(c: HttpMultipartRequestCustomizer) => void} callback
|
|
5274
5332
|
*/
|
|
5275
5333
|
multipart(callback) {
|
|
5276
5334
|
const formData = new FormData();
|
|
@@ -5435,7 +5493,7 @@ class HttpMultipartRequestCustomizer {
|
|
|
5435
5493
|
* @returns this builder
|
|
5436
5494
|
*/
|
|
5437
5495
|
blobs(name, values) {
|
|
5438
|
-
for (
|
|
5496
|
+
for (const v of values) {
|
|
5439
5497
|
this.#formData.append(name, v);
|
|
5440
5498
|
}
|
|
5441
5499
|
return this;
|
|
@@ -5470,7 +5528,16 @@ class LocalStorage extends Storage {
|
|
|
5470
5528
|
}
|
|
5471
5529
|
static load(k) {
|
|
5472
5530
|
const got = localStorage.getItem(k);
|
|
5473
|
-
|
|
5531
|
+
if (got === null) {
|
|
5532
|
+
return undefined;
|
|
5533
|
+
}
|
|
5534
|
+
try {
|
|
5535
|
+
return JSON.parse(got);
|
|
5536
|
+
} catch {
|
|
5537
|
+
//not what save wrote: drop it, otherwise every later read fails the same way
|
|
5538
|
+
localStorage.removeItem(k);
|
|
5539
|
+
return undefined;
|
|
5540
|
+
}
|
|
5474
5541
|
}
|
|
5475
5542
|
static remove(k) {
|
|
5476
5543
|
localStorage.removeItem(k);
|
|
@@ -5488,7 +5555,16 @@ class SessionStorage extends Storage {
|
|
|
5488
5555
|
}
|
|
5489
5556
|
static load(k) {
|
|
5490
5557
|
const got = sessionStorage.getItem(k);
|
|
5491
|
-
|
|
5558
|
+
if (got === null) {
|
|
5559
|
+
return undefined;
|
|
5560
|
+
}
|
|
5561
|
+
try {
|
|
5562
|
+
return JSON.parse(got);
|
|
5563
|
+
} catch {
|
|
5564
|
+
//not what save wrote: drop it, otherwise every later read fails the same way
|
|
5565
|
+
sessionStorage.removeItem(k);
|
|
5566
|
+
return undefined;
|
|
5567
|
+
}
|
|
5492
5568
|
}
|
|
5493
5569
|
static remove(k) {
|
|
5494
5570
|
sessionStorage.removeItem(k);
|
|
@@ -5510,7 +5586,7 @@ class VersionedLocalStorage {
|
|
|
5510
5586
|
return undefined;
|
|
5511
5587
|
}
|
|
5512
5588
|
if (stored.revision !== revision) {
|
|
5513
|
-
|
|
5589
|
+
LocalStorage.remove(key);
|
|
5514
5590
|
return undefined;
|
|
5515
5591
|
}
|
|
5516
5592
|
return stored.data;
|
|
@@ -5527,7 +5603,7 @@ class VersionedSessionStorage {
|
|
|
5527
5603
|
return undefined;
|
|
5528
5604
|
}
|
|
5529
5605
|
if (stored.revision !== revision) {
|
|
5530
|
-
|
|
5606
|
+
SessionStorage.remove(key);
|
|
5531
5607
|
return undefined;
|
|
5532
5608
|
}
|
|
5533
5609
|
return stored.data;
|
|
@@ -5662,7 +5738,7 @@ class Timing {
|
|
|
5662
5738
|
*/
|
|
5663
5739
|
static debounce(timeoutMs, func, options) {
|
|
5664
5740
|
const opts = options ?? Timing.DEBOUNCE_DEFAULT;
|
|
5665
|
-
let tid = null;
|
|
5741
|
+
let tid = /** @type {number | null} */ (null);
|
|
5666
5742
|
let args = [];
|
|
5667
5743
|
let previousTimestamp = 0;
|
|
5668
5744
|
|
|
@@ -5682,8 +5758,8 @@ class Timing {
|
|
|
5682
5758
|
}
|
|
5683
5759
|
};
|
|
5684
5760
|
|
|
5685
|
-
const debounced =
|
|
5686
|
-
args =
|
|
5761
|
+
const debounced = (...called) => {
|
|
5762
|
+
args = called;
|
|
5687
5763
|
previousTimestamp = performance.now();
|
|
5688
5764
|
if (tid === null) {
|
|
5689
5765
|
tid = setTimeout(later, timeoutMs);
|
|
@@ -5692,7 +5768,11 @@ class Timing {
|
|
|
5692
5768
|
}
|
|
5693
5769
|
}
|
|
5694
5770
|
};
|
|
5695
|
-
const abort = () =>
|
|
5771
|
+
const abort = () => {
|
|
5772
|
+
clearTimeout(tid ?? undefined);
|
|
5773
|
+
tid = null;
|
|
5774
|
+
args = [];
|
|
5775
|
+
};
|
|
5696
5776
|
return [debounced, abort];
|
|
5697
5777
|
}
|
|
5698
5778
|
static THROTTLE_DEFAULT = 0;
|
|
@@ -5707,7 +5787,7 @@ class Timing {
|
|
|
5707
5787
|
*/
|
|
5708
5788
|
static throttle(timeoutMs, func, options) {
|
|
5709
5789
|
const opts = options ?? Timing.THROTTLE_DEFAULT;
|
|
5710
|
-
let tid = null;
|
|
5790
|
+
let tid = /** @type {number | null} */ (null);
|
|
5711
5791
|
let args = [];
|
|
5712
5792
|
let previousTimestamp = 0;
|
|
5713
5793
|
|
|
@@ -5719,13 +5799,13 @@ class Timing {
|
|
|
5719
5799
|
args = [];
|
|
5720
5800
|
}
|
|
5721
5801
|
};
|
|
5722
|
-
const throttled =
|
|
5802
|
+
const throttled = (...called) => {
|
|
5723
5803
|
const now = performance.now();
|
|
5724
5804
|
if (!previousTimestamp && opts & Timing.THROTTLE_NO_LEADING) {
|
|
5725
5805
|
previousTimestamp = now;
|
|
5726
5806
|
}
|
|
5727
5807
|
const remaining = previousTimestamp === 0 ? 0 : timeoutMs - (now - previousTimestamp);
|
|
5728
|
-
args =
|
|
5808
|
+
args = called;
|
|
5729
5809
|
if (remaining <= 0 || remaining > timeoutMs) {
|
|
5730
5810
|
if (tid !== null) {
|
|
5731
5811
|
clearTimeout(tid);
|
|
@@ -5740,7 +5820,11 @@ class Timing {
|
|
|
5740
5820
|
tid = setTimeout(later, remaining);
|
|
5741
5821
|
}
|
|
5742
5822
|
};
|
|
5743
|
-
const abort = () =>
|
|
5823
|
+
const abort = () => {
|
|
5824
|
+
clearTimeout(tid ?? undefined);
|
|
5825
|
+
tid = null;
|
|
5826
|
+
args = [];
|
|
5827
|
+
};
|
|
5744
5828
|
return [throttled, abort];
|
|
5745
5829
|
}
|
|
5746
5830
|
}
|
|
@@ -5754,7 +5838,7 @@ class Bindings {
|
|
|
5754
5838
|
*/
|
|
5755
5839
|
static flatten(obj, prefix, stops) {
|
|
5756
5840
|
return Object.keys(obj).reduce((acc, k) => {
|
|
5757
|
-
const pre = prefix.length ? prefix
|
|
5841
|
+
const pre = prefix.length ? `${prefix}.${k}` : k;
|
|
5758
5842
|
if (!stops.has(pre) && typeof obj[k] === 'object' && obj[k] !== null) {
|
|
5759
5843
|
Object.assign(acc, Bindings.flatten(obj[k], pre, stops));
|
|
5760
5844
|
} else {
|
|
@@ -5772,7 +5856,7 @@ class Bindings {
|
|
|
5772
5856
|
static providePath(result, path, value) {
|
|
5773
5857
|
const keys = path.split('.').map((k) => (/^[0-9]+$/.test(k) ? +k : k));
|
|
5774
5858
|
let current = result ?? {};
|
|
5775
|
-
let previous = null;
|
|
5859
|
+
let previous = /** @type {any} */ (null);
|
|
5776
5860
|
for (let i = 0; ; ++i) {
|
|
5777
5861
|
const ckey = keys[i];
|
|
5778
5862
|
const pkey = keys[i - 1];
|
|
@@ -5805,12 +5889,12 @@ class Bindings {
|
|
|
5805
5889
|
if (!el.checked) {
|
|
5806
5890
|
return undefined;
|
|
5807
5891
|
}
|
|
5808
|
-
return el.dataset
|
|
5892
|
+
return el.dataset.fulBindType === 'boolean' ? el.value === 'true' : el.value;
|
|
5809
5893
|
}
|
|
5810
5894
|
if (el.getAttribute('type') === 'checkbox') {
|
|
5811
5895
|
return el.checked;
|
|
5812
5896
|
}
|
|
5813
|
-
if (el.dataset
|
|
5897
|
+
if (el.dataset.fulBindType === 'boolean') {
|
|
5814
5898
|
return !el.value ? null : el.value === 'true';
|
|
5815
5899
|
}
|
|
5816
5900
|
if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
|
|
@@ -5843,12 +5927,14 @@ class Bindings {
|
|
|
5843
5927
|
|
|
5844
5928
|
/**
|
|
5845
5929
|
*
|
|
5846
|
-
* @param {Element
|
|
5930
|
+
* @param {Element & {dataset?: any} & {checked?: boolean} & {value?: any}} el
|
|
5847
5931
|
* @returns
|
|
5848
5932
|
*/
|
|
5849
5933
|
static mutate(el, raw) {
|
|
5850
5934
|
if (el.getAttribute('type') === 'radio') {
|
|
5851
|
-
|
|
5935
|
+
//values are matched as strings, as ful-radio-group does: extract decodes
|
|
5936
|
+
//boolean radios, and payloads carry numbers where the attribute is text
|
|
5937
|
+
el.checked = raw != null && el.getAttribute('value') === String(raw);
|
|
5852
5938
|
return;
|
|
5853
5939
|
}
|
|
5854
5940
|
if (el.getAttribute('type') === 'checkbox') {
|
|
@@ -5872,7 +5958,9 @@ class Bindings {
|
|
|
5872
5958
|
static errors(form, es, scrollOnError) {
|
|
5873
5959
|
const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
|
|
5874
5960
|
const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
5875
|
-
form.querySelectorAll(`[name]`).forEach((el) =>
|
|
5961
|
+
form.querySelectorAll(`[name]`).forEach((el) => {
|
|
5962
|
+
el.setCustomValidity?.('');
|
|
5963
|
+
});
|
|
5876
5964
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
5877
5965
|
el.replaceChildren();
|
|
5878
5966
|
el.setAttribute('hidden', '');
|
|
@@ -5880,12 +5968,12 @@ class Bindings {
|
|
|
5880
5968
|
fieldErrors.forEach((e) => {
|
|
5881
5969
|
const name = e.context.replace(/\[/g, '.').replace(/\]\./g, '.').replace(/\]/g, '');
|
|
5882
5970
|
const parts = name.split('.');
|
|
5883
|
-
for (let i = parts.length; i
|
|
5971
|
+
for (let i = parts.length; i !== 0; --i) {
|
|
5884
5972
|
const prefix = parts.slice(0, i).join('.');
|
|
5885
5973
|
const suffix = parts.slice(i, parts.length).join('.');
|
|
5886
|
-
form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) =>
|
|
5887
|
-
input.setCustomValidity?.(e.reason, suffix)
|
|
5888
|
-
);
|
|
5974
|
+
form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) => {
|
|
5975
|
+
input.setCustomValidity?.(e.reason, suffix);
|
|
5976
|
+
});
|
|
5889
5977
|
}
|
|
5890
5978
|
});
|
|
5891
5979
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
@@ -5895,7 +5983,7 @@ class Bindings {
|
|
|
5895
5983
|
el.removeAttribute('hidden');
|
|
5896
5984
|
}
|
|
5897
5985
|
});
|
|
5898
|
-
if (es.length
|
|
5986
|
+
if (es.length === 0 || !scrollOnError) {
|
|
5899
5987
|
return;
|
|
5900
5988
|
}
|
|
5901
5989
|
Array.from(form.querySelectorAll(`:invalid`))
|
|
@@ -5967,7 +6055,8 @@ class FormLoader {
|
|
|
5967
6055
|
class Form extends ParsedElement {
|
|
5968
6056
|
form;
|
|
5969
6057
|
render() {
|
|
5970
|
-
const form =
|
|
6058
|
+
const form = document.createElement('form');
|
|
6059
|
+
this.form = form;
|
|
5971
6060
|
form.setAttribute('novalidate', '');
|
|
5972
6061
|
Attributes.forward('form-', this, form);
|
|
5973
6062
|
form.replaceChildren(...this.childNodes);
|
|
@@ -5990,50 +6079,53 @@ class Form extends ParsedElement {
|
|
|
5990
6079
|
*/
|
|
5991
6080
|
async submit(submitter) {
|
|
5992
6081
|
this.spinner(true);
|
|
6082
|
+
//one try: building the loader and preparing the request are as much part of a
|
|
6083
|
+
//submit as sending it, and a mapper that throws is how a caller reports a
|
|
6084
|
+
//problem with the values
|
|
6085
|
+
let values;
|
|
6086
|
+
let request;
|
|
5993
6087
|
try {
|
|
5994
6088
|
const loader = registry.component(this.getAttribute('loader') ?? 'loaders:form').create(this);
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
6089
|
+
values = Bindings.extractFrom(this.form, submitter);
|
|
6090
|
+
request = await loader.prepare(values, this);
|
|
6091
|
+
const se = new CustomEvent('submit', {
|
|
6092
|
+
bubbles: true,
|
|
6093
|
+
cancelable: true,
|
|
6094
|
+
detail: { submitter, values, request },
|
|
6095
|
+
});
|
|
6096
|
+
if (!this.dispatchEvent(se)) {
|
|
6097
|
+
return;
|
|
6098
|
+
}
|
|
6099
|
+
this.errors = [];
|
|
6100
|
+
const sre = new CustomEvent('submit:requested', {
|
|
6101
|
+
bubbles: true,
|
|
6102
|
+
cancelable: false,
|
|
6103
|
+
detail: { submitter, values: se.detail.values, request: se.detail.request },
|
|
6104
|
+
});
|
|
6105
|
+
let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
|
|
6106
|
+
request = sre.detail.request;
|
|
6107
|
+
|
|
6108
|
+
response = await loader.submit(request, this, response);
|
|
6109
|
+
const mapped = await loader.transform(response, this);
|
|
6110
|
+
this.dispatchEvent(
|
|
6111
|
+
new CustomEvent('submit:success', {
|
|
5999
6112
|
bubbles: true,
|
|
6000
|
-
cancelable:
|
|
6001
|
-
detail: { submitter, values, request },
|
|
6002
|
-
})
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
const sre = new CustomEvent('submit:requested', {
|
|
6113
|
+
cancelable: false,
|
|
6114
|
+
detail: { submitter, values, request, response: mapped },
|
|
6115
|
+
}),
|
|
6116
|
+
);
|
|
6117
|
+
} catch (e) {
|
|
6118
|
+
this.dispatchEvent(
|
|
6119
|
+
new CustomEvent('submit:failure', {
|
|
6008
6120
|
bubbles: true,
|
|
6009
6121
|
cancelable: false,
|
|
6010
|
-
detail: { submitter, values
|
|
6011
|
-
})
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
response = await loader.submit(request, this, response);
|
|
6016
|
-
const mapped = await loader.transform(response, this);
|
|
6017
|
-
this.dispatchEvent(
|
|
6018
|
-
new CustomEvent('submit:success', {
|
|
6019
|
-
bubbles: true,
|
|
6020
|
-
cancelable: false,
|
|
6021
|
-
detail: { submitter, values, request, response: mapped },
|
|
6022
|
-
}),
|
|
6023
|
-
);
|
|
6024
|
-
} catch (e) {
|
|
6025
|
-
this.dispatchEvent(
|
|
6026
|
-
new CustomEvent('submit:failure', {
|
|
6027
|
-
bubbles: true,
|
|
6028
|
-
cancelable: false,
|
|
6029
|
-
detail: { submitter, values, request, exception: e },
|
|
6030
|
-
}),
|
|
6031
|
-
);
|
|
6032
|
-
if (e instanceof Failure) {
|
|
6033
|
-
this.errors = e.problems;
|
|
6034
|
-
}
|
|
6035
|
-
console.warn('failed to submit form', this, 'reason:', e);
|
|
6122
|
+
detail: { submitter, values, request, exception: e },
|
|
6123
|
+
}),
|
|
6124
|
+
);
|
|
6125
|
+
if (e instanceof Failure) {
|
|
6126
|
+
this.errors = e.problems;
|
|
6036
6127
|
}
|
|
6128
|
+
console.warn('failed to submit form', this, 'reason:', e);
|
|
6037
6129
|
} finally {
|
|
6038
6130
|
this.spinner(false);
|
|
6039
6131
|
}
|
|
@@ -6041,7 +6133,20 @@ class Form extends ParsedElement {
|
|
|
6041
6133
|
reset() {
|
|
6042
6134
|
this.form.reset();
|
|
6043
6135
|
}
|
|
6136
|
+
#spinning = 0;
|
|
6044
6137
|
spinner(spin) {
|
|
6138
|
+
//submits can overlap: only the outermost one saves and restores the button states
|
|
6139
|
+
if (spin) {
|
|
6140
|
+
++this.#spinning;
|
|
6141
|
+
if (this.#spinning !== 1) {
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
} else {
|
|
6145
|
+
this.#spinning = Math.max(0, this.#spinning - 1);
|
|
6146
|
+
if (this.#spinning !== 0) {
|
|
6147
|
+
return;
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6045
6150
|
this.querySelectorAll('ful-spinner').forEach((el) => {
|
|
6046
6151
|
const hel = /** @type HTMLElement */ (el);
|
|
6047
6152
|
hel.hidden = !spin;
|
|
@@ -6072,7 +6177,7 @@ class Form extends ParsedElement {
|
|
|
6072
6177
|
}
|
|
6073
6178
|
|
|
6074
6179
|
class Input extends ParsedElement {
|
|
6075
|
-
static observed = ['value', 'readonly:presence', 'required:presence'];
|
|
6180
|
+
static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
6076
6181
|
static slots = true;
|
|
6077
6182
|
static template = `
|
|
6078
6183
|
<div class="form-label">
|
|
@@ -6109,12 +6214,6 @@ class Input extends ParsedElement {
|
|
|
6109
6214
|
this._input = fragment.querySelector('input,textarea');
|
|
6110
6215
|
|
|
6111
6216
|
Attributes.forward('input-', this, this._input);
|
|
6112
|
-
if (!skipObservedSetup) {
|
|
6113
|
-
this.disabled = disabled;
|
|
6114
|
-
this.readonly = observed.readonly;
|
|
6115
|
-
this.required = observed.required;
|
|
6116
|
-
this.value = observed.value;
|
|
6117
|
-
}
|
|
6118
6217
|
this._input.addEventListener('keydown', (evt) => {
|
|
6119
6218
|
if (evt.key !== 'Enter' || this._type() === 'textarea') {
|
|
6120
6219
|
return;
|
|
@@ -6130,19 +6229,26 @@ class Input extends ParsedElement {
|
|
|
6130
6229
|
form.requestSubmit(submitter);
|
|
6131
6230
|
});
|
|
6132
6231
|
this._input.addEventListener('input', (evt) => {
|
|
6133
|
-
const
|
|
6134
|
-
if (!
|
|
6232
|
+
const mask = this.getAttribute('mask');
|
|
6233
|
+
if (!mask) {
|
|
6135
6234
|
return;
|
|
6136
6235
|
}
|
|
6236
|
+
const strip = (v) => v.replace(new RegExp(mask, 'g'), '');
|
|
6137
6237
|
const before = evt.target.value;
|
|
6138
|
-
const after = before
|
|
6238
|
+
const after = strip(before);
|
|
6139
6239
|
if (before === after) {
|
|
6140
6240
|
return;
|
|
6141
6241
|
}
|
|
6142
6242
|
const start = evt.target.selectionStart;
|
|
6143
|
-
const offset = before.length - after.length;
|
|
6144
6243
|
evt.target.value = after;
|
|
6145
|
-
|
|
6244
|
+
if (start === null) {
|
|
6245
|
+
//email, number and the date types have no selection to restore
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
//the caret keeps its place among the characters that survived, so only the
|
|
6249
|
+
//ones stripped before it count
|
|
6250
|
+
const caret = strip(before.slice(0, start)).length;
|
|
6251
|
+
evt.target.setSelectionRange(caret, caret);
|
|
6146
6252
|
});
|
|
6147
6253
|
this._input.addEventListener('change', (evt) => {
|
|
6148
6254
|
evt.stopPropagation();
|
|
@@ -6162,6 +6268,15 @@ class Input extends ParsedElement {
|
|
|
6162
6268
|
this._input.ariaDescribedByElements = [this._fieldError];
|
|
6163
6269
|
this._input.ariaLabelledByElements = [label];
|
|
6164
6270
|
this.replaceChildren(fragment);
|
|
6271
|
+
if (!skipObservedSetup) {
|
|
6272
|
+
// biome-ignore lint/complexity/noUselessThisAlias: keeps checkJs from seeing these as class fields
|
|
6273
|
+
const el = this;
|
|
6274
|
+
el.disabled = disabled;
|
|
6275
|
+
el.readonly = observed.readonly;
|
|
6276
|
+
el.required = observed.required;
|
|
6277
|
+
el.placeholder = observed.placeholder;
|
|
6278
|
+
el.value = observed.value;
|
|
6279
|
+
}
|
|
6165
6280
|
}
|
|
6166
6281
|
get value() {
|
|
6167
6282
|
const uppercase = this.hasAttribute('uppercase');
|
|
@@ -6188,6 +6303,11 @@ class Input extends ParsedElement {
|
|
|
6188
6303
|
}
|
|
6189
6304
|
set disabled(d) {
|
|
6190
6305
|
Attributes.toggle(this._input, 'disabled', d);
|
|
6306
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
6307
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
6308
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
6309
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
6310
|
+
Attributes.toggle(this, 'disabled', d);
|
|
6191
6311
|
}
|
|
6192
6312
|
get required() {
|
|
6193
6313
|
return this._input.getAttribute('aria-required') === 'true';
|
|
@@ -6198,6 +6318,18 @@ class Input extends ParsedElement {
|
|
|
6198
6318
|
Attributes.toggle(this, 'required', d);
|
|
6199
6319
|
});
|
|
6200
6320
|
}
|
|
6321
|
+
get placeholder() {
|
|
6322
|
+
const v = this._input.getAttribute('placeholder');
|
|
6323
|
+
return v === ' ' ? null : v;
|
|
6324
|
+
}
|
|
6325
|
+
set placeholder(d) {
|
|
6326
|
+
//without a placeholder :placeholder-shown never matches, and floating labels
|
|
6327
|
+
//rely on it, so a blank one stands in for none
|
|
6328
|
+
Attributes.set(this._input, 'placeholder', d ?? ' ');
|
|
6329
|
+
this.reflect(() => {
|
|
6330
|
+
Attributes.set(this, 'placeholder', d);
|
|
6331
|
+
});
|
|
6332
|
+
}
|
|
6201
6333
|
focus(options) {
|
|
6202
6334
|
this._input.focus(options);
|
|
6203
6335
|
}
|
|
@@ -6217,23 +6349,23 @@ class Input extends ParsedElement {
|
|
|
6217
6349
|
|
|
6218
6350
|
class LocalDate extends ParsedElement {
|
|
6219
6351
|
render() {
|
|
6220
|
-
const content = this.
|
|
6352
|
+
const content = this.textContent.trim();
|
|
6221
6353
|
if (content === '') {
|
|
6222
|
-
this.
|
|
6354
|
+
this.replaceChildren(this.getAttribute('default') ?? '');
|
|
6223
6355
|
return;
|
|
6224
6356
|
}
|
|
6225
6357
|
const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
6226
6358
|
const formatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' });
|
|
6227
6359
|
const [y, m, d] = content.split('-').map(Number);
|
|
6228
|
-
this.
|
|
6360
|
+
this.replaceChildren(formatter.format(new Date(y, m - 1, d)));
|
|
6229
6361
|
}
|
|
6230
6362
|
}
|
|
6231
6363
|
|
|
6232
6364
|
class Instant extends ParsedElement {
|
|
6233
6365
|
render() {
|
|
6234
|
-
const content = this.
|
|
6366
|
+
const content = this.textContent.trim();
|
|
6235
6367
|
if (content === '') {
|
|
6236
|
-
this.
|
|
6368
|
+
this.replaceChildren(this.getAttribute('default') ?? '');
|
|
6237
6369
|
return;
|
|
6238
6370
|
}
|
|
6239
6371
|
const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
@@ -6246,7 +6378,7 @@ class Instant extends ParsedElement {
|
|
|
6246
6378
|
second: 'numeric',
|
|
6247
6379
|
hour12: false,
|
|
6248
6380
|
});
|
|
6249
|
-
this.
|
|
6381
|
+
this.replaceChildren(format.format(new Date(Instant.isoToLocal(content))));
|
|
6250
6382
|
}
|
|
6251
6383
|
static isoToLocal(iso) {
|
|
6252
6384
|
//this is so sad
|
|
@@ -6259,16 +6391,17 @@ class Instant extends ParsedElement {
|
|
|
6259
6391
|
}
|
|
6260
6392
|
|
|
6261
6393
|
class InputLocalDate extends Input {
|
|
6262
|
-
static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
|
|
6394
|
+
static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
|
|
6263
6395
|
_type() {
|
|
6264
6396
|
return 'date';
|
|
6265
6397
|
}
|
|
6266
6398
|
render(conf) {
|
|
6267
6399
|
const { observed } = conf;
|
|
6268
6400
|
super.render(conf);
|
|
6401
|
+
//step first: on a time input min and max are snapped to its grid
|
|
6402
|
+
this.step = observed.step;
|
|
6269
6403
|
this.min = observed.min;
|
|
6270
6404
|
this.max = observed.max;
|
|
6271
|
-
this.step = observed.step;
|
|
6272
6405
|
}
|
|
6273
6406
|
get min() {
|
|
6274
6407
|
const v = this._input.min;
|
|
@@ -6314,13 +6447,14 @@ class InputLocalDate extends Input {
|
|
|
6314
6447
|
case 'd':
|
|
6315
6448
|
r.setDate(r.getDate() + offset * sign);
|
|
6316
6449
|
break;
|
|
6317
|
-
case 'm':
|
|
6450
|
+
case 'm': {
|
|
6318
6451
|
const originalDay = r.getDate();
|
|
6319
6452
|
r.setMonth(r.getMonth() + offset * sign);
|
|
6320
6453
|
if (r.getDate() !== originalDay) {
|
|
6321
6454
|
r.setDate(0);
|
|
6322
6455
|
}
|
|
6323
6456
|
break;
|
|
6457
|
+
}
|
|
6324
6458
|
case 'y':
|
|
6325
6459
|
r.setFullYear(r.getFullYear() + offset * sign);
|
|
6326
6460
|
break;
|
|
@@ -6333,10 +6467,62 @@ class InputLocalTime extends InputLocalDate {
|
|
|
6333
6467
|
_type() {
|
|
6334
6468
|
return 'time';
|
|
6335
6469
|
}
|
|
6470
|
+
get min() {
|
|
6471
|
+
const v = this._input.min;
|
|
6472
|
+
return v === '' ? null : v;
|
|
6473
|
+
}
|
|
6474
|
+
set min(v) {
|
|
6475
|
+
this._input.min = this.#fromNowOrOffset(v);
|
|
6476
|
+
}
|
|
6477
|
+
get max() {
|
|
6478
|
+
const v = this._input.max;
|
|
6479
|
+
return v === '' ? null : v;
|
|
6480
|
+
}
|
|
6481
|
+
set max(v) {
|
|
6482
|
+
this._input.max = this.#fromNowOrOffset(v);
|
|
6483
|
+
}
|
|
6484
|
+
/**
|
|
6485
|
+
* Resolves `now` and hour or minute offsets against the current time, wrapping
|
|
6486
|
+
* around midnight. `m` is minutes here, unlike the date offsets of the parent where
|
|
6487
|
+
* it is months: months mean nothing on a time. Anything else is passed through.
|
|
6488
|
+
*/
|
|
6489
|
+
#fromNowOrOffset(v) {
|
|
6490
|
+
if (!v) {
|
|
6491
|
+
return '';
|
|
6492
|
+
}
|
|
6493
|
+
const resolved = new Date();
|
|
6494
|
+
if (v !== 'now') {
|
|
6495
|
+
const re = /^([+-])(\d+)([hm])$/;
|
|
6496
|
+
const match = re.exec(v);
|
|
6497
|
+
if (!match) {
|
|
6498
|
+
return v;
|
|
6499
|
+
}
|
|
6500
|
+
const sign = match[1] === '-' ? -1 : 1;
|
|
6501
|
+
const offset = +match[2] * sign;
|
|
6502
|
+
if (match[3] === 'h') {
|
|
6503
|
+
resolved.setHours(resolved.getHours() + offset);
|
|
6504
|
+
} else {
|
|
6505
|
+
resolved.setMinutes(resolved.getMinutes() + offset);
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
return InputLocalTime.#snapped(resolved, Number(this._input.step) || 60);
|
|
6509
|
+
}
|
|
6510
|
+
/**
|
|
6511
|
+
* Truncates a time to the step grid: min anchors that grid, so a bound that is not
|
|
6512
|
+
* on it makes every value on it invalid.
|
|
6513
|
+
*/
|
|
6514
|
+
static #snapped(date, stepSeconds) {
|
|
6515
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
6516
|
+
const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
|
|
6517
|
+
const snapped = Math.floor(seconds / stepSeconds) * stepSeconds;
|
|
6518
|
+
const hh = pad(Math.floor(snapped / 3600));
|
|
6519
|
+
const mm = pad(Math.floor((snapped % 3600) / 60));
|
|
6520
|
+
return stepSeconds % 60 === 0 ? `${hh}:${mm}` : `${hh}:${mm}:${pad(snapped % 60)}`;
|
|
6521
|
+
}
|
|
6336
6522
|
}
|
|
6337
6523
|
|
|
6338
6524
|
class InputInstant extends Input {
|
|
6339
|
-
static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
|
|
6525
|
+
static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
|
|
6340
6526
|
_type() {
|
|
6341
6527
|
return 'datetime-local';
|
|
6342
6528
|
}
|
|
@@ -6381,28 +6567,28 @@ class InputFile extends Input {
|
|
|
6381
6567
|
static l10n = {
|
|
6382
6568
|
en: {
|
|
6383
6569
|
dropzonelabel: 'Click or drop your files here',
|
|
6384
|
-
|
|
6570
|
+
unacceptablefiletype: 'Only files of type {0} are supported',
|
|
6385
6571
|
maxfilesizeexceeded: 'Maximum supported file size is {0}',
|
|
6386
6572
|
maxtotalsizeexceeded: 'Maximum supported total file size is {0}',
|
|
6387
6573
|
maxfilesexceeded: 'Maximum files count exceeded',
|
|
6388
6574
|
},
|
|
6389
6575
|
it: {
|
|
6390
6576
|
dropzonelabel: 'Clicca o trascina i file qui',
|
|
6391
|
-
|
|
6577
|
+
unacceptablefiletype: 'Solo i file di tipo {0} sono supportati',
|
|
6392
6578
|
maxfilesizeexceeded: 'La dimensione massima di un file è di {0}',
|
|
6393
6579
|
maxtotalsizeexceeded: 'La dimensione massima complessiva dei file è di {0}',
|
|
6394
6580
|
maxfilesexceeded: 'Numero massimo di file superato',
|
|
6395
6581
|
},
|
|
6396
6582
|
es: {
|
|
6397
6583
|
dropzonelabel: 'Haz clic o arrastra tus archivos aquí',
|
|
6398
|
-
|
|
6584
|
+
unacceptablefiletype: 'Solo se admiten archivos de tipo {0}',
|
|
6399
6585
|
maxfilesizeexceeded: 'El tamaño máximo de archivo admitido es {0}',
|
|
6400
6586
|
maxtotalsizeexceeded: 'El tamaño total máximo admitido es {0}',
|
|
6401
6587
|
maxfilesexceeded: 'Se ha superado el número máximo de archivos',
|
|
6402
6588
|
},
|
|
6403
6589
|
fr: {
|
|
6404
6590
|
dropzonelabel: 'Cliquez ou déposez vos fichiers ici',
|
|
6405
|
-
|
|
6591
|
+
unacceptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
|
|
6406
6592
|
maxfilesizeexceeded: 'La taille maximale de fichier prise en charge est {0}',
|
|
6407
6593
|
maxtotalsizeexceeded: 'La taille totale maximale prise en charge est {0}',
|
|
6408
6594
|
maxfilesexceeded: 'Nombre maximal de fichiers dépassé',
|
|
@@ -6412,6 +6598,7 @@ class InputFile extends Input {
|
|
|
6412
6598
|
'value',
|
|
6413
6599
|
'readonly:presence',
|
|
6414
6600
|
'required:presence',
|
|
6601
|
+
'placeholder',
|
|
6415
6602
|
'accept:csv',
|
|
6416
6603
|
'multiple:presence',
|
|
6417
6604
|
'itemlist:presence',
|
|
@@ -6461,7 +6648,7 @@ class InputFile extends Input {
|
|
|
6461
6648
|
};
|
|
6462
6649
|
render(conf) {
|
|
6463
6650
|
const { observed } = conf;
|
|
6464
|
-
super.render(conf);
|
|
6651
|
+
super.render({ ...conf, skipObservedSetup: true });
|
|
6465
6652
|
this.#items = this.querySelector('ful-item-list');
|
|
6466
6653
|
this.#dropzone = this.querySelector('[data-ref=dropzone]');
|
|
6467
6654
|
this.#warnings = this.querySelector('ful-field-warnings');
|
|
@@ -6472,6 +6659,12 @@ class InputFile extends Input {
|
|
|
6472
6659
|
this.maxfiles = observed.maxfiles;
|
|
6473
6660
|
this.maxfilesize = observed.maxfilesize;
|
|
6474
6661
|
this.maxtotalsize = observed.maxtotalsize;
|
|
6662
|
+
|
|
6663
|
+
this.disabled = conf.disabled;
|
|
6664
|
+
this.readonly = observed.readonly;
|
|
6665
|
+
this.required = observed.required;
|
|
6666
|
+
this.placeholder = observed.placeholder;
|
|
6667
|
+
this.value = observed.value;
|
|
6475
6668
|
this.#warnings.addEventListener('animationend', (e) => {
|
|
6476
6669
|
e.target.remove();
|
|
6477
6670
|
});
|
|
@@ -6479,11 +6672,17 @@ class InputFile extends Input {
|
|
|
6479
6672
|
if (!e.target.closest('button')) {
|
|
6480
6673
|
return;
|
|
6481
6674
|
}
|
|
6482
|
-
const
|
|
6675
|
+
const idx = [...this.#items.children].indexOf(e.target.closest('ful-item'));
|
|
6676
|
+
if (idx === -1) {
|
|
6677
|
+
return;
|
|
6678
|
+
}
|
|
6483
6679
|
const dt = new DataTransfer();
|
|
6484
|
-
[...this.files]
|
|
6680
|
+
[...this.files]
|
|
6681
|
+
.filter((f, i) => i !== idx)
|
|
6682
|
+
.forEach((f) => {
|
|
6683
|
+
dt.items.add(f);
|
|
6684
|
+
});
|
|
6485
6685
|
this.files = dt.files;
|
|
6486
|
-
this.#update();
|
|
6487
6686
|
});
|
|
6488
6687
|
this.#dropzone.addEventListener('click', (e) => {
|
|
6489
6688
|
this.querySelector('input')?.click();
|
|
@@ -6494,10 +6693,15 @@ class InputFile extends Input {
|
|
|
6494
6693
|
});
|
|
6495
6694
|
this.#dropzone.addEventListener('drop', (e) => {
|
|
6496
6695
|
e.preventDefault();
|
|
6696
|
+
const dropped = [...e.dataTransfer.items].filter((i) => i.kind === 'file');
|
|
6697
|
+
if (dropped.length === 0) {
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6497
6700
|
const dt = new DataTransfer();
|
|
6498
|
-
|
|
6701
|
+
dropped.forEach((i) => {
|
|
6702
|
+
dt.items.add(i.getAsFile());
|
|
6703
|
+
});
|
|
6499
6704
|
this.files = dt.files;
|
|
6500
|
-
this.#update();
|
|
6501
6705
|
});
|
|
6502
6706
|
this._input.addEventListener('change', (e) => {
|
|
6503
6707
|
this.#update();
|
|
@@ -6512,6 +6716,7 @@ class InputFile extends Input {
|
|
|
6512
6716
|
}
|
|
6513
6717
|
#update() {
|
|
6514
6718
|
this.setCustomValidity();
|
|
6719
|
+
this.#warnings.replaceChildren();
|
|
6515
6720
|
this.#ensureAcceptable();
|
|
6516
6721
|
this.#ensureFileSizes();
|
|
6517
6722
|
this.#ensureTotalSize();
|
|
@@ -6522,7 +6727,7 @@ class InputFile extends Input {
|
|
|
6522
6727
|
.renderTo(this.#items);
|
|
6523
6728
|
}
|
|
6524
6729
|
warning(key, args) {
|
|
6525
|
-
this.template('warning').withOverlay({ key, args }).
|
|
6730
|
+
this.template('warning').withOverlay({ key, args }).appendTo(this.#warnings);
|
|
6526
6731
|
}
|
|
6527
6732
|
#ensureAcceptable() {
|
|
6528
6733
|
if (!this.#accept.length) {
|
|
@@ -6535,10 +6740,14 @@ class InputFile extends Input {
|
|
|
6535
6740
|
if (unacceptable.length === 0) {
|
|
6536
6741
|
return;
|
|
6537
6742
|
}
|
|
6538
|
-
this.warning('
|
|
6743
|
+
this.warning('unacceptablefiletype', this.#accept.join(', '));
|
|
6539
6744
|
const dt = new DataTransfer();
|
|
6540
|
-
[...this.files]
|
|
6541
|
-
|
|
6745
|
+
[...this.files]
|
|
6746
|
+
.filter((f) => !unacceptable.includes(f))
|
|
6747
|
+
.forEach((f) => {
|
|
6748
|
+
dt.items.add(f);
|
|
6749
|
+
});
|
|
6750
|
+
this._input.files = dt.files;
|
|
6542
6751
|
}
|
|
6543
6752
|
#ensureFilesCount() {
|
|
6544
6753
|
if (this.#maxfiles === null) {
|
|
@@ -6548,8 +6757,7 @@ class InputFile extends Input {
|
|
|
6548
6757
|
return;
|
|
6549
6758
|
}
|
|
6550
6759
|
this.warning('maxfilesexceeded');
|
|
6551
|
-
|
|
6552
|
-
this.files = dt.files;
|
|
6760
|
+
this._input.files = new DataTransfer().files;
|
|
6553
6761
|
}
|
|
6554
6762
|
|
|
6555
6763
|
#ensureFileSizes() {
|
|
@@ -6562,8 +6770,12 @@ class InputFile extends Input {
|
|
|
6562
6770
|
}
|
|
6563
6771
|
this.warning('maxfilesizeexceeded', this.#formatByteSize(this.#maxfilesize));
|
|
6564
6772
|
const dt = new DataTransfer();
|
|
6565
|
-
[...this.files]
|
|
6566
|
-
|
|
6773
|
+
[...this.files]
|
|
6774
|
+
.filter((f) => !oversized.includes(f))
|
|
6775
|
+
.forEach((f) => {
|
|
6776
|
+
dt.items.add(f);
|
|
6777
|
+
});
|
|
6778
|
+
this._input.files = dt.files;
|
|
6567
6779
|
}
|
|
6568
6780
|
#ensureTotalSize() {
|
|
6569
6781
|
if (this.#maxtotalsize === null) {
|
|
@@ -6574,7 +6786,7 @@ class InputFile extends Input {
|
|
|
6574
6786
|
return;
|
|
6575
6787
|
}
|
|
6576
6788
|
this.warning('maxtotalsizeexceeded', this.#formatByteSize(this.#maxtotalsize));
|
|
6577
|
-
this.files = new DataTransfer().files;
|
|
6789
|
+
this._input.files = new DataTransfer().files;
|
|
6578
6790
|
}
|
|
6579
6791
|
get accept() {
|
|
6580
6792
|
return this.#accept;
|
|
@@ -6600,6 +6812,7 @@ class InputFile extends Input {
|
|
|
6600
6812
|
}
|
|
6601
6813
|
set files(vs) {
|
|
6602
6814
|
this._input.files = vs;
|
|
6815
|
+
this.#update();
|
|
6603
6816
|
}
|
|
6604
6817
|
get file() {
|
|
6605
6818
|
return this.files[0] ?? null;
|
|
@@ -6620,7 +6833,6 @@ class InputFile extends Input {
|
|
|
6620
6833
|
return;
|
|
6621
6834
|
}
|
|
6622
6835
|
this.files = new DataTransfer().files;
|
|
6623
|
-
this.#update();
|
|
6624
6836
|
}
|
|
6625
6837
|
get totalsize() {
|
|
6626
6838
|
return Array.from(this.files).reduce((a, f) => a + f.size, 0);
|
|
@@ -6787,7 +6999,7 @@ class SelectLoader {
|
|
|
6787
6999
|
const http = registry.component('http-client');
|
|
6788
7000
|
const responseMapper = SelectLoader.#responseMapperFrom(el);
|
|
6789
7001
|
|
|
6790
|
-
if ('chunked'
|
|
7002
|
+
if ('chunked' === el.getAttribute('mode')) {
|
|
6791
7003
|
return new PartialRemoteLoader({
|
|
6792
7004
|
http,
|
|
6793
7005
|
url: el.getAttribute('src'),
|
|
@@ -6863,8 +7075,14 @@ class Dropdown extends ParsedElement {
|
|
|
6863
7075
|
});
|
|
6864
7076
|
this.replaceChildren(fragment);
|
|
6865
7077
|
}
|
|
7078
|
+
#selected() {
|
|
7079
|
+
return this.#menu?.querySelector('[selected]') ?? this.#menu?.firstElementChild ?? null;
|
|
7080
|
+
}
|
|
6866
7081
|
acceptSelection() {
|
|
6867
|
-
const selected = this.#
|
|
7082
|
+
const selected = this.#selected();
|
|
7083
|
+
if (!selected) {
|
|
7084
|
+
return;
|
|
7085
|
+
}
|
|
6868
7086
|
this.#change(selected);
|
|
6869
7087
|
}
|
|
6870
7088
|
update(values) {
|
|
@@ -6900,6 +7118,9 @@ class Dropdown extends ParsedElement {
|
|
|
6900
7118
|
try {
|
|
6901
7119
|
const data = await loader();
|
|
6902
7120
|
this.update(data);
|
|
7121
|
+
} catch (e) {
|
|
7122
|
+
this.hide();
|
|
7123
|
+
throw e;
|
|
6903
7124
|
} finally {
|
|
6904
7125
|
this.#spinner.setAttribute('hidden', '');
|
|
6905
7126
|
this.#menu.removeAttribute('hidden');
|
|
@@ -6907,9 +7128,9 @@ class Dropdown extends ParsedElement {
|
|
|
6907
7128
|
}
|
|
6908
7129
|
async moveOrShow(forward, loader) {
|
|
6909
7130
|
if (this.shown) {
|
|
6910
|
-
const selected = this.#
|
|
6911
|
-
const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
6912
|
-
if (candidate) {
|
|
7131
|
+
const selected = this.#selected();
|
|
7132
|
+
const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
7133
|
+
if (selected && candidate) {
|
|
6913
7134
|
selected.removeAttribute('selected');
|
|
6914
7135
|
candidate.setAttribute('selected', '');
|
|
6915
7136
|
candidate.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
@@ -6962,6 +7183,7 @@ class Select extends ParsedElement {
|
|
|
6962
7183
|
#multiple;
|
|
6963
7184
|
#fieldError;
|
|
6964
7185
|
#values = new Map();
|
|
7186
|
+
#token = 0;
|
|
6965
7187
|
constructor() {
|
|
6966
7188
|
super();
|
|
6967
7189
|
this.internals = this.attachInternals();
|
|
@@ -6974,7 +7196,11 @@ class Select extends ParsedElement {
|
|
|
6974
7196
|
.create(this, { options: slots.options });
|
|
6975
7197
|
|
|
6976
7198
|
this.#multiple = this.hasAttribute('multiple');
|
|
6977
|
-
|
|
7199
|
+
try {
|
|
7200
|
+
await this.#loader.prefetch?.();
|
|
7201
|
+
} catch (/** @type any */ e) {
|
|
7202
|
+
console.warn('failed to prefetch select options', this, 'reason:', e);
|
|
7203
|
+
}
|
|
6978
7204
|
const fragment = this.template().withOverlay({ slots, name }).render();
|
|
6979
7205
|
this.#input = fragment.querySelector('input');
|
|
6980
7206
|
this.#items = fragment.querySelector('ful-item-list');
|
|
@@ -6993,11 +7219,9 @@ class Select extends ParsedElement {
|
|
|
6993
7219
|
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
6994
7220
|
this.#input.ariaDescribedByElements = [this.#fieldError];
|
|
6995
7221
|
this.#input.ariaLabelledByElements = [label];
|
|
6996
|
-
|
|
6997
|
-
const self = this;
|
|
6998
7222
|
const [dload, abortdload] = Timing.throttle(400, () => {
|
|
6999
|
-
|
|
7000
|
-
|
|
7223
|
+
this.#input.setAttribute('aria-expanded', 'true');
|
|
7224
|
+
this.#ddmenu.show(() => this.#loader.load(this.#input.value));
|
|
7001
7225
|
});
|
|
7002
7226
|
this.addEventListener('click', (/** @type any */ e) => {
|
|
7003
7227
|
if (e.target.matches('input')) {
|
|
@@ -7064,13 +7288,13 @@ class Select extends ParsedElement {
|
|
|
7064
7288
|
case 'ArrowUp': {
|
|
7065
7289
|
e.preventDefault();
|
|
7066
7290
|
this.#input.setAttribute('aria-expanded', 'true');
|
|
7067
|
-
this.#ddmenu.moveOrShow(false, () =>
|
|
7291
|
+
this.#ddmenu.moveOrShow(false, () => this.#loader.load(this.#input.value));
|
|
7068
7292
|
break;
|
|
7069
7293
|
}
|
|
7070
7294
|
case 'ArrowDown': {
|
|
7071
7295
|
e.preventDefault();
|
|
7072
7296
|
this.#input.setAttribute('aria-expanded', 'true');
|
|
7073
|
-
this.#ddmenu.moveOrShow(true, () =>
|
|
7297
|
+
this.#ddmenu.moveOrShow(true, () => this.#loader.load(this.#input.value));
|
|
7074
7298
|
break;
|
|
7075
7299
|
}
|
|
7076
7300
|
case 'Escape': {
|
|
@@ -7079,6 +7303,12 @@ class Select extends ParsedElement {
|
|
|
7079
7303
|
break;
|
|
7080
7304
|
}
|
|
7081
7305
|
case 'Enter': {
|
|
7306
|
+
if (!this.#ddmenu.shown) {
|
|
7307
|
+
//nothing to accept: submit the form as ful-input does. the inner
|
|
7308
|
+
//input carries form="" so it never submits one on its own
|
|
7309
|
+
this.#requestSubmit();
|
|
7310
|
+
return;
|
|
7311
|
+
}
|
|
7082
7312
|
e.preventDefault();
|
|
7083
7313
|
this.#input.setAttribute('aria-expanded', 'false');
|
|
7084
7314
|
this.#ddmenu.acceptSelection();
|
|
@@ -7127,6 +7357,16 @@ class Select extends ParsedElement {
|
|
|
7127
7357
|
async withLoader(fn) {
|
|
7128
7358
|
return await fn(this.#loader);
|
|
7129
7359
|
}
|
|
7360
|
+
#requestSubmit() {
|
|
7361
|
+
const form = this.internals.form;
|
|
7362
|
+
if (!form) {
|
|
7363
|
+
return;
|
|
7364
|
+
}
|
|
7365
|
+
const candidates = /** @type [HTMLButtonElement|HTMLInputElement] */ (
|
|
7366
|
+
Array.from(form.querySelectorAll('button:not(:disabled), input:not(:disabled)'))
|
|
7367
|
+
);
|
|
7368
|
+
form.requestSubmit(candidates.find((el) => el.type === 'submit'));
|
|
7369
|
+
}
|
|
7130
7370
|
#changed() {
|
|
7131
7371
|
const selection = [...this.#values.entries()].map((e) => ({
|
|
7132
7372
|
key: e[0],
|
|
@@ -7156,16 +7396,43 @@ class Select extends ParsedElement {
|
|
|
7156
7396
|
this.template('items').withOverlay({ entries: this.#values.entries() }).renderTo(this.#items);
|
|
7157
7397
|
}
|
|
7158
7398
|
set value(vs) {
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7399
|
+
//the csvm mapper yields [] for a missing multiple value, an empty string is
|
|
7400
|
+
//left alone: it is a usable key for an <option value="">
|
|
7401
|
+
const keys = vs == null ? [] : Array.isArray(vs) ? vs : [vs];
|
|
7402
|
+
//the keys are known synchronously and are all `value` reads, so they are applied
|
|
7403
|
+
//now: only the labels need the loader, until then a key stands in for its own
|
|
7404
|
+
this.#values = new Map(keys.map((k) => [k, [k]]));
|
|
7405
|
+
this.#syncBadges();
|
|
7406
|
+
const token = ++this.#token;
|
|
7407
|
+
if (keys.length === 0) {
|
|
7162
7408
|
return;
|
|
7163
7409
|
}
|
|
7164
|
-
(
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7410
|
+
this.#resolve(keys, token);
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Resolves the labels of the assigned keys. A failed lookup is left to reject so
|
|
7414
|
+
* that it is reported like any other failure: the keys stay applied either way.
|
|
7415
|
+
*/
|
|
7416
|
+
async #resolve(keys, token) {
|
|
7417
|
+
const entries = await this.#loader.exact(...keys);
|
|
7418
|
+
if (token !== this.#token) {
|
|
7419
|
+
//a newer assignment has been made in the meantime
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
//label the keys that are still selected: a removal made while the lookup was in
|
|
7423
|
+
//flight must not be undone by it, and a key the loader does not know is dropped
|
|
7424
|
+
const resolved = new Map(entries.map((e) => [e[0], e.slice(1)]));
|
|
7425
|
+
for (const key of keys) {
|
|
7426
|
+
if (!this.#values.has(key)) {
|
|
7427
|
+
continue;
|
|
7428
|
+
}
|
|
7429
|
+
if (resolved.has(key)) {
|
|
7430
|
+
this.#values.set(key, resolved.get(key));
|
|
7431
|
+
} else {
|
|
7432
|
+
this.#values.delete(key);
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
this.#syncBadges();
|
|
7169
7436
|
}
|
|
7170
7437
|
get value() {
|
|
7171
7438
|
if (this.#multiple) {
|
|
@@ -7184,6 +7451,11 @@ class Select extends ParsedElement {
|
|
|
7184
7451
|
}
|
|
7185
7452
|
set disabled(d) {
|
|
7186
7453
|
Attributes.toggle(this.#input, 'disabled', d);
|
|
7454
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
7455
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
7456
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
7457
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
7458
|
+
Attributes.toggle(this, 'disabled', d);
|
|
7187
7459
|
}
|
|
7188
7460
|
get readonly() {
|
|
7189
7461
|
return this.#input.readOnly;
|
|
@@ -7289,7 +7561,9 @@ class RadioGroup extends ParsedElement {
|
|
|
7289
7561
|
return [input, label];
|
|
7290
7562
|
});
|
|
7291
7563
|
|
|
7292
|
-
radioEls.forEach((el) =>
|
|
7564
|
+
radioEls.forEach((el) => {
|
|
7565
|
+
el.remove();
|
|
7566
|
+
});
|
|
7293
7567
|
this.template().withOverlay({ name, slots, inputsAndLabels }).renderTo(this);
|
|
7294
7568
|
this.#fieldset = this.firstElementChild;
|
|
7295
7569
|
this.disabled = disabled;
|
|
@@ -7333,6 +7607,11 @@ class RadioGroup extends ParsedElement {
|
|
|
7333
7607
|
}
|
|
7334
7608
|
set disabled(d) {
|
|
7335
7609
|
Attributes.toggle(this.#fieldset, 'disabled', d);
|
|
7610
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
7611
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
7612
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
7613
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
7614
|
+
Attributes.toggle(this, 'disabled', d);
|
|
7336
7615
|
}
|
|
7337
7616
|
get required() {
|
|
7338
7617
|
return this.#fieldset.getAttribute('aria-required') === 'true';
|
|
@@ -7382,7 +7661,7 @@ class Checkbox extends ParsedElement {
|
|
|
7382
7661
|
this.internals.role = 'presentation';
|
|
7383
7662
|
}
|
|
7384
7663
|
render({ slots, observed, disabled }) {
|
|
7385
|
-
const isSwitch = this.getAttribute('type')
|
|
7664
|
+
const isSwitch = this.getAttribute('type') === 'switch';
|
|
7386
7665
|
const klass = isSwitch ? 'form-check form-switch' : 'form-check';
|
|
7387
7666
|
const fragment = this.template().withOverlay({ slots, klass, isSwitch }).render();
|
|
7388
7667
|
this.#container = fragment.firstElementChild;
|
|
@@ -7446,6 +7725,11 @@ class Checkbox extends ParsedElement {
|
|
|
7446
7725
|
}
|
|
7447
7726
|
set disabled(d) {
|
|
7448
7727
|
Attributes.toggle(this.#input, 'disabled', d);
|
|
7728
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
7729
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
7730
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
7731
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
7732
|
+
Attributes.toggle(this, 'disabled', d);
|
|
7449
7733
|
}
|
|
7450
7734
|
get required() {
|
|
7451
7735
|
return this.#input.getAttribute('aria-required') === 'true';
|
|
@@ -7486,9 +7770,10 @@ class Spinner extends ParsedElement {
|
|
|
7486
7770
|
class SortButton extends ParsedElement {
|
|
7487
7771
|
static observed = ['order'];
|
|
7488
7772
|
#order;
|
|
7489
|
-
render() {
|
|
7773
|
+
render({ observed }) {
|
|
7490
7774
|
const sorter = this.getAttribute('sorter');
|
|
7491
7775
|
const orders = ['asc', 'desc', null];
|
|
7776
|
+
this.order = observed.order;
|
|
7492
7777
|
this.addEventListener('click', () => {
|
|
7493
7778
|
const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
|
|
7494
7779
|
this.dispatchEvent(
|
|
@@ -7578,10 +7863,12 @@ class Pagination extends ParsedElement {
|
|
|
7578
7863
|
#total = 0;
|
|
7579
7864
|
#current = 0;
|
|
7580
7865
|
render({ observed }) {
|
|
7581
|
-
this.
|
|
7866
|
+
this.total = observed.total ?? 0;
|
|
7867
|
+
this.current = observed.current ?? 0;
|
|
7582
7868
|
this.addEventListener('click', (/** @type any */ evt) => {
|
|
7583
7869
|
const el = evt.target.closest('a');
|
|
7584
|
-
if (!el) {
|
|
7870
|
+
if (!el || el.classList.contains('disabled')) {
|
|
7871
|
+
//a disabled link leads nowhere: the page it would ask for does not exist
|
|
7585
7872
|
return;
|
|
7586
7873
|
}
|
|
7587
7874
|
this.dispatchEvent(
|
|
@@ -7597,25 +7884,20 @@ class Pagination extends ParsedElement {
|
|
|
7597
7884
|
}
|
|
7598
7885
|
update(current, total) {
|
|
7599
7886
|
const maxRender = Number(this.getAttribute('pages') ?? '5');
|
|
7600
|
-
const
|
|
7887
|
+
const hasPrev = current > 0;
|
|
7888
|
+
const hasNext = current + 1 < total;
|
|
7889
|
+
//a disabled arrow carries no page: there is nothing valid for it to point at
|
|
7890
|
+
const prev = { index: hasPrev ? current - 1 : null, enabled: hasPrev };
|
|
7601
7891
|
const curr = { index: current, label: current + 1 };
|
|
7602
|
-
const next = { index:
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
if (p >= 0) {
|
|
7612
|
-
pages.unshift({ index: p, label: p + 1 });
|
|
7613
|
-
}
|
|
7614
|
-
const n = mid + offset;
|
|
7615
|
-
if (n < total) {
|
|
7616
|
-
pages.push({ index: n, label: n + 1 });
|
|
7617
|
-
}
|
|
7618
|
-
}
|
|
7892
|
+
const next = { index: hasNext ? current + 1 : null, enabled: hasNext };
|
|
7893
|
+
//the window holds at most maxRender pages, centered on the current one and slid
|
|
7894
|
+
//back towards the end so it stays full on the last pages
|
|
7895
|
+
const rendered = Math.max(1, Math.min(maxRender, total));
|
|
7896
|
+
const first = Math.max(0, Math.min(current - Math.floor((rendered - 1) / 2), total - rendered));
|
|
7897
|
+
const pages = Array.from({ length: rendered }, (_, offset) => ({
|
|
7898
|
+
index: first + offset,
|
|
7899
|
+
label: first + offset + 1,
|
|
7900
|
+
}));
|
|
7619
7901
|
this.template().withOverlay({ total, prev, curr, next, pages }).renderTo(this);
|
|
7620
7902
|
}
|
|
7621
7903
|
get total() {
|
|
@@ -7642,9 +7924,10 @@ class Pagination extends ParsedElement {
|
|
|
7642
7924
|
|
|
7643
7925
|
class TableSchemaParser {
|
|
7644
7926
|
static parse(nodeOrFragment, template) {
|
|
7645
|
-
|
|
7927
|
+
//nodeOrFragment is undefined when the slot is missing altogether
|
|
7928
|
+
const schema = nodeOrFragment ? Nodes.queryChildren(nodeOrFragment, 'schema') : null;
|
|
7646
7929
|
if (!schema) {
|
|
7647
|
-
throw new Error(
|
|
7930
|
+
throw new Error('missing expected <schema>: ful-table needs a <template slot="schema"> holding one');
|
|
7648
7931
|
}
|
|
7649
7932
|
const headersTr = document.createElement('tr');
|
|
7650
7933
|
const rowsTr = document.createElement('tr');
|
|
@@ -7863,9 +8146,10 @@ class Table extends ParsedElement {
|
|
|
7863
8146
|
this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
|
|
7864
8147
|
this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
|
|
7865
8148
|
this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
|
|
7866
|
-
this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
|
|
7867
8149
|
this.replaceChildren(fragment);
|
|
7868
|
-
|
|
8150
|
+
const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
|
|
8151
|
+
schema.headersTemplate.renderTo(thead);
|
|
8152
|
+
this.#sorters = thead.querySelectorAll('ful-sorter');
|
|
7869
8153
|
await Rendering.waitForChildren(this);
|
|
7870
8154
|
|
|
7871
8155
|
const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
|
|
@@ -7900,11 +8184,16 @@ class Table extends ParsedElement {
|
|
|
7900
8184
|
this.addEventListener('sort-requested', async (/** @type any */ e) => {
|
|
7901
8185
|
const sortRequest = e.detail.value.order ? e.detail.value : null;
|
|
7902
8186
|
await this.load(this.#latestRequest.pageRequest, sortRequest, this.#latestRequest.filterRequest);
|
|
7903
|
-
this.#sorters.forEach((s) =>
|
|
8187
|
+
this.#sorters.forEach((s) => {
|
|
8188
|
+
s.order = null;
|
|
8189
|
+
});
|
|
7904
8190
|
e.target.order = e.detail.value.order;
|
|
7905
8191
|
});
|
|
7906
8192
|
if (this.hasAttribute('autoload')) {
|
|
7907
|
-
|
|
8193
|
+
//not awaited: the first load must not hold up the upgrade, and a loader that
|
|
8194
|
+
//fails or never answers must not keep ftl:ready from firing for the page.
|
|
8195
|
+
//load renders its own error state and lets the failure reject, so it is reported
|
|
8196
|
+
this.reload();
|
|
7908
8197
|
}
|
|
7909
8198
|
}
|
|
7910
8199
|
|
|
@@ -7917,7 +8206,7 @@ class Table extends ParsedElement {
|
|
|
7917
8206
|
}
|
|
7918
8207
|
async load(pageRequest, sortRequest, filterRequest) {
|
|
7919
8208
|
this.#body.replaceChildren();
|
|
7920
|
-
this.#loading.removeAttribute('hidden'
|
|
8209
|
+
this.#loading.removeAttribute('hidden');
|
|
7921
8210
|
this.#feedback.setAttribute('hidden', '');
|
|
7922
8211
|
this.#noAutoload.setAttribute('hidden', '');
|
|
7923
8212
|
try {
|
|
@@ -7926,7 +8215,7 @@ class Table extends ParsedElement {
|
|
|
7926
8215
|
this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
|
|
7927
8216
|
} catch (/** @type any */ error) {
|
|
7928
8217
|
this.#loading.setAttribute('hidden', '');
|
|
7929
|
-
this.#feedback.removeAttribute('hidden'
|
|
8218
|
+
this.#feedback.removeAttribute('hidden');
|
|
7930
8219
|
if (!error.problems) {
|
|
7931
8220
|
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
|
|
7932
8221
|
} else {
|
|
@@ -7968,7 +8257,7 @@ class Table extends ParsedElement {
|
|
|
7968
8257
|
}
|
|
7969
8258
|
|
|
7970
8259
|
class InstantFilter extends Input {
|
|
7971
|
-
static observed = ['value:json', 'readonly:presence', 'required:presence'];
|
|
8260
|
+
static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
7972
8261
|
static template = `
|
|
7973
8262
|
<div class="form-label">
|
|
7974
8263
|
<label>{{{{ slots.default }}}}</label>
|
|
@@ -8002,10 +8291,16 @@ class InstantFilter extends Input {
|
|
|
8002
8291
|
this.#operator = this.querySelector('[data-ref=operator]');
|
|
8003
8292
|
this.#value1 = this.querySelector('[data-ref=value1]');
|
|
8004
8293
|
this.#value2 = this.querySelector('[data-ref=value2]');
|
|
8294
|
+
//Input.render only re-dispatches changes coming from the first operand
|
|
8295
|
+
this.#value2.addEventListener('change', (evt) => {
|
|
8296
|
+
evt.stopPropagation();
|
|
8297
|
+
this.#notifyChange();
|
|
8298
|
+
});
|
|
8005
8299
|
|
|
8006
8300
|
this.disabled = conf.disabled;
|
|
8007
8301
|
this.readonly = conf.observed.readonly;
|
|
8008
8302
|
this.required = conf.observed.required;
|
|
8303
|
+
this.placeholder = conf.observed.placeholder;
|
|
8009
8304
|
this.value = conf.observed.value;
|
|
8010
8305
|
|
|
8011
8306
|
this.addEventListener('click', (evt) => {
|
|
@@ -8015,9 +8310,13 @@ class InstantFilter extends Input {
|
|
|
8015
8310
|
}
|
|
8016
8311
|
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
8017
8312
|
const value = /** @type String */ (target.getAttribute('value'));
|
|
8313
|
+
const previous = btn.getAttribute('value');
|
|
8018
8314
|
Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
8019
8315
|
btn.setAttribute('value', value);
|
|
8020
8316
|
btn.innerHTML = target.innerHTML;
|
|
8317
|
+
if (previous !== value) {
|
|
8318
|
+
this.#notifyChange();
|
|
8319
|
+
}
|
|
8021
8320
|
});
|
|
8022
8321
|
}
|
|
8023
8322
|
|
|
@@ -8033,14 +8332,40 @@ class InstantFilter extends Input {
|
|
|
8033
8332
|
return;
|
|
8034
8333
|
}
|
|
8035
8334
|
const [operator, ...values] = v;
|
|
8036
|
-
this.#
|
|
8335
|
+
this.#showOperator(operator);
|
|
8037
8336
|
this.#value1.value = values[0] ? Instant.isoToLocal(values[0]) : values[0];
|
|
8038
8337
|
this.#value2.value = values[1] ? Instant.isoToLocal(values[1]) : values[1];
|
|
8039
8338
|
}
|
|
8339
|
+
#showOperator(operator) {
|
|
8340
|
+
this.#operator.setAttribute('value', operator);
|
|
8341
|
+
const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
|
|
8342
|
+
const item = items.find((a) => a.getAttribute('value') === operator);
|
|
8343
|
+
if (item) {
|
|
8344
|
+
this.#operator.innerHTML = item.innerHTML;
|
|
8345
|
+
}
|
|
8346
|
+
Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
|
|
8347
|
+
}
|
|
8348
|
+
#notifyChange() {
|
|
8349
|
+
this.dispatchEvent(
|
|
8350
|
+
new CustomEvent('change', {
|
|
8351
|
+
bubbles: true,
|
|
8352
|
+
cancelable: false,
|
|
8353
|
+
detail: {
|
|
8354
|
+
value: this.value,
|
|
8355
|
+
},
|
|
8356
|
+
}),
|
|
8357
|
+
);
|
|
8358
|
+
}
|
|
8359
|
+
get readonly() {
|
|
8360
|
+
return super.readonly;
|
|
8361
|
+
}
|
|
8040
8362
|
set readonly(v) {
|
|
8041
8363
|
this.#value2.readOnly = v;
|
|
8042
8364
|
super.readonly = v;
|
|
8043
8365
|
}
|
|
8366
|
+
get disabled() {
|
|
8367
|
+
return super.disabled;
|
|
8368
|
+
}
|
|
8044
8369
|
set disabled(d) {
|
|
8045
8370
|
Attributes.toggle(this.#value2, 'disabled', d);
|
|
8046
8371
|
super.disabled = d;
|
|
@@ -8048,7 +8373,7 @@ class InstantFilter extends Input {
|
|
|
8048
8373
|
}
|
|
8049
8374
|
|
|
8050
8375
|
class LocalDateFilter extends Input {
|
|
8051
|
-
static observed = ['value:json', 'readonly:presence', 'required:presence'];
|
|
8376
|
+
static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
8052
8377
|
static template = `
|
|
8053
8378
|
<div class="form-label">
|
|
8054
8379
|
<label>{{{{ slots.default }}}}</label>
|
|
@@ -8083,10 +8408,16 @@ class LocalDateFilter extends Input {
|
|
|
8083
8408
|
this.#operator = this.querySelector('[data-ref=operator]');
|
|
8084
8409
|
this.#value1 = this.querySelector('[data-ref=value1]');
|
|
8085
8410
|
this.#value2 = this.querySelector('[data-ref=value2]');
|
|
8411
|
+
//Input.render only re-dispatches changes coming from the first operand
|
|
8412
|
+
this.#value2.addEventListener('change', (evt) => {
|
|
8413
|
+
evt.stopPropagation();
|
|
8414
|
+
this.#notifyChange();
|
|
8415
|
+
});
|
|
8086
8416
|
|
|
8087
8417
|
this.disabled = conf.disabled;
|
|
8088
8418
|
this.readonly = conf.observed.readonly;
|
|
8089
8419
|
this.required = conf.observed.required;
|
|
8420
|
+
this.placeholder = conf.observed.placeholder;
|
|
8090
8421
|
this.value = conf.observed.value;
|
|
8091
8422
|
|
|
8092
8423
|
this.addEventListener('click', (evt) => {
|
|
@@ -8096,14 +8427,18 @@ class LocalDateFilter extends Input {
|
|
|
8096
8427
|
}
|
|
8097
8428
|
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
8098
8429
|
const value = /** @type String */ (target.getAttribute('value'));
|
|
8430
|
+
const previous = btn.getAttribute('value');
|
|
8099
8431
|
Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
8100
8432
|
btn.setAttribute('value', value);
|
|
8101
8433
|
btn.innerHTML = target.innerHTML;
|
|
8434
|
+
if (previous !== value) {
|
|
8435
|
+
this.#notifyChange();
|
|
8436
|
+
}
|
|
8102
8437
|
});
|
|
8103
8438
|
}
|
|
8104
8439
|
get value() {
|
|
8105
8440
|
const operator = this.#operator.getAttribute('value');
|
|
8106
|
-
const values = operator
|
|
8441
|
+
const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
|
|
8107
8442
|
return values.some((v) => v === '') ? undefined : [operator, ...values];
|
|
8108
8443
|
}
|
|
8109
8444
|
set value(v) {
|
|
@@ -8113,14 +8448,40 @@ class LocalDateFilter extends Input {
|
|
|
8113
8448
|
return;
|
|
8114
8449
|
}
|
|
8115
8450
|
const [operator, ...values] = v;
|
|
8116
|
-
this.#
|
|
8451
|
+
this.#showOperator(operator);
|
|
8117
8452
|
this.#value1.value = values[0];
|
|
8118
8453
|
this.#value2.value = values[1];
|
|
8119
8454
|
}
|
|
8455
|
+
#showOperator(operator) {
|
|
8456
|
+
this.#operator.setAttribute('value', operator);
|
|
8457
|
+
const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
|
|
8458
|
+
const item = items.find((a) => a.getAttribute('value') === operator);
|
|
8459
|
+
if (item) {
|
|
8460
|
+
this.#operator.innerHTML = item.innerHTML;
|
|
8461
|
+
}
|
|
8462
|
+
Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
|
|
8463
|
+
}
|
|
8464
|
+
#notifyChange() {
|
|
8465
|
+
this.dispatchEvent(
|
|
8466
|
+
new CustomEvent('change', {
|
|
8467
|
+
bubbles: true,
|
|
8468
|
+
cancelable: false,
|
|
8469
|
+
detail: {
|
|
8470
|
+
value: this.value,
|
|
8471
|
+
},
|
|
8472
|
+
}),
|
|
8473
|
+
);
|
|
8474
|
+
}
|
|
8475
|
+
get readonly() {
|
|
8476
|
+
return super.readonly;
|
|
8477
|
+
}
|
|
8120
8478
|
set readonly(v) {
|
|
8121
8479
|
this.#value2.readOnly = v;
|
|
8122
8480
|
super.readonly = v;
|
|
8123
8481
|
}
|
|
8482
|
+
get disabled() {
|
|
8483
|
+
return super.disabled;
|
|
8484
|
+
}
|
|
8124
8485
|
set disabled(d) {
|
|
8125
8486
|
Attributes.toggle(this.#value2, 'disabled', d);
|
|
8126
8487
|
super.disabled = d;
|
|
@@ -8128,7 +8489,7 @@ class LocalDateFilter extends Input {
|
|
|
8128
8489
|
}
|
|
8129
8490
|
|
|
8130
8491
|
class TextFilter extends Input {
|
|
8131
|
-
static observed = ['value:json', 'readonly:presence', 'required:presence'];
|
|
8492
|
+
static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
8132
8493
|
static template = `
|
|
8133
8494
|
<div class="form-label">
|
|
8134
8495
|
<label>{{{{ slots.default }}}}</label>
|
|
@@ -8152,6 +8513,8 @@ class TextFilter extends Input {
|
|
|
8152
8513
|
`;
|
|
8153
8514
|
#operator;
|
|
8154
8515
|
#value;
|
|
8516
|
+
//the sensitivity has no control of its own: it is carried through from whoever set the value
|
|
8517
|
+
#sensitivity = 'IGNORE_CASE';
|
|
8155
8518
|
render(conf) {
|
|
8156
8519
|
super.render({ ...conf, skipObservedSetup: true });
|
|
8157
8520
|
|
|
@@ -8161,6 +8524,7 @@ class TextFilter extends Input {
|
|
|
8161
8524
|
this.disabled = conf.disabled;
|
|
8162
8525
|
this.readonly = conf.observed.readonly;
|
|
8163
8526
|
this.required = conf.observed.required;
|
|
8527
|
+
this.placeholder = conf.observed.placeholder;
|
|
8164
8528
|
this.value = conf.observed.value;
|
|
8165
8529
|
|
|
8166
8530
|
this.addEventListener('click', (evt) => {
|
|
@@ -8170,13 +8534,17 @@ class TextFilter extends Input {
|
|
|
8170
8534
|
}
|
|
8171
8535
|
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
8172
8536
|
const value = /** @type String */ (target.getAttribute('value'));
|
|
8537
|
+
const previous = btn.getAttribute('value');
|
|
8173
8538
|
btn.setAttribute('value', value);
|
|
8174
8539
|
btn.innerHTML = target.innerHTML;
|
|
8540
|
+
if (previous !== value) {
|
|
8541
|
+
this.#notifyChange();
|
|
8542
|
+
}
|
|
8175
8543
|
});
|
|
8176
8544
|
}
|
|
8177
8545
|
get value() {
|
|
8178
8546
|
const operator = this.#operator.getAttribute('value');
|
|
8179
|
-
return this.#value.value === '' ? undefined : [operator,
|
|
8547
|
+
return this.#value.value === '' ? undefined : [operator, this.#sensitivity, this.#value.value];
|
|
8180
8548
|
}
|
|
8181
8549
|
set value(v) {
|
|
8182
8550
|
if (v == null) {
|
|
@@ -8184,15 +8552,35 @@ class TextFilter extends Input {
|
|
|
8184
8552
|
return;
|
|
8185
8553
|
}
|
|
8186
8554
|
const [operator, sensitivity, value] = v;
|
|
8187
|
-
this.#
|
|
8555
|
+
this.#showOperator(operator);
|
|
8556
|
+
this.#sensitivity = sensitivity ?? 'IGNORE_CASE';
|
|
8188
8557
|
this.#value.value = value;
|
|
8189
8558
|
}
|
|
8559
|
+
#showOperator(operator) {
|
|
8560
|
+
this.#operator.setAttribute('value', operator);
|
|
8561
|
+
const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
|
|
8562
|
+
const item = items.find((a) => a.getAttribute('value') === operator);
|
|
8563
|
+
if (item) {
|
|
8564
|
+
this.#operator.innerHTML = item.innerHTML;
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
8567
|
+
#notifyChange() {
|
|
8568
|
+
this.dispatchEvent(
|
|
8569
|
+
new CustomEvent('change', {
|
|
8570
|
+
bubbles: true,
|
|
8571
|
+
cancelable: false,
|
|
8572
|
+
detail: {
|
|
8573
|
+
value: this.value,
|
|
8574
|
+
},
|
|
8575
|
+
}),
|
|
8576
|
+
);
|
|
8577
|
+
}
|
|
8190
8578
|
}
|
|
8191
8579
|
|
|
8192
8580
|
class LocalizationModule {
|
|
8193
8581
|
static t(k, ...args) {
|
|
8194
|
-
//@ts-
|
|
8195
|
-
const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.
|
|
8582
|
+
//@ts-expect-error l10n and language come from the element class and the data stack
|
|
8583
|
+
const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.en?.[k] ?? k;
|
|
8196
8584
|
if (args.length === 0) {
|
|
8197
8585
|
return format;
|
|
8198
8586
|
}
|
|
@@ -8275,9 +8663,11 @@ var ful = /*#__PURE__*/Object.freeze({
|
|
|
8275
8663
|
});
|
|
8276
8664
|
|
|
8277
8665
|
if (typeof window !== 'undefined') {
|
|
8666
|
+
// biome-ignore-start lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
|
|
8278
8667
|
window['ftl'] = ftl;
|
|
8279
8668
|
window['httpc'] = httpc;
|
|
8280
8669
|
window['ful'] = ful;
|
|
8670
|
+
// biome-ignore-end lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
|
|
8281
8671
|
}
|
|
8282
8672
|
|
|
8283
8673
|
export { AsyncEvents, Attributes, Base64, Bindings, Checkbox, Dropdown, ExpressionEvaluator, Expressions, Failure, Form, FormLoader, Fragments, Hex, HttpClient, HttpClientError, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LightSlots, LocalDate, LocalDateFilter, LocalStorage, LocalizationModule, MediaType, Nodes, Pagination, ParsedElement, Plugin, RadioGroup, Registry, RenderError, Rendering, Select, SelectLoader, SessionStorage, SortButton, Spinner, Table, TableSchemaParser, Template, Templates, TextFilter, Timing, VersionedLocalStorage, VersionedSessionStorage, registry };
|