@optionfactory/fml 8.0.0 → 8.0.2
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/custom-elements.json +1210 -0
- 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 +628 -209
- 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 +628 -209
- package/dist/fml.mjs.map +1 -1
- package/dist/ftl.d.mts +2 -1
- package/dist/ftl.iife.js +90 -34
- 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 +90 -34
- 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/dist/vscode.html-custom-data.json +357 -0
- package/dist/web-types.json +1006 -0
- package/package.json +11 -9
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,
|
|
@@ -4373,16 +4378,39 @@ class UpgradeQueue {
|
|
|
4373
4378
|
);
|
|
4374
4379
|
});
|
|
4375
4380
|
}
|
|
4381
|
+
#finished = new Map();
|
|
4376
4382
|
enqueue(el) {
|
|
4377
4383
|
if (this.#q.has(el)) {
|
|
4378
4384
|
//already upgrading, can happen when disconnecting an element
|
|
4379
4385
|
//while it's already queued for upgrade (e.g.: ful-form)
|
|
4380
4386
|
return;
|
|
4381
4387
|
}
|
|
4388
|
+
//settling waits on a signal that only ever resolves, so nothing here attaches a
|
|
4389
|
+
//rejection handler to the upgrade itself: a component that fails is still
|
|
4390
|
+
//reported the way it always was
|
|
4391
|
+
const { promise: finished, resolve: markFinished } = /** @type {PromiseWithResolvers<void>} */ (
|
|
4392
|
+
Promise.withResolvers()
|
|
4393
|
+
);
|
|
4382
4394
|
const promise = Nodes.waitParsed(el)
|
|
4383
4395
|
.then(() => el.upgrade())
|
|
4384
|
-
.finally(() =>
|
|
4396
|
+
.finally(() => {
|
|
4397
|
+
this.#q.delete(el);
|
|
4398
|
+
this.#finished.delete(el);
|
|
4399
|
+
markFinished();
|
|
4400
|
+
});
|
|
4385
4401
|
this.#q.set(el, promise);
|
|
4402
|
+
this.#finished.set(el, finished);
|
|
4403
|
+
}
|
|
4404
|
+
/**
|
|
4405
|
+
* Waits for every queued upgrade to settle, including the ones enqueued while
|
|
4406
|
+
* waiting: a component is only queued once its parent connects it, so a single pass
|
|
4407
|
+
* would miss everything nested. A component that fails to upgrade does not hold the
|
|
4408
|
+
* others back, readiness means the queue drained rather than that everything worked.
|
|
4409
|
+
*/
|
|
4410
|
+
async settle() {
|
|
4411
|
+
while (this.#finished.size !== 0) {
|
|
4412
|
+
await Promise.all(Array.from(this.#finished.values()));
|
|
4413
|
+
}
|
|
4386
4414
|
}
|
|
4387
4415
|
get entries() {
|
|
4388
4416
|
return this.#q.entries();
|
|
@@ -4578,18 +4606,30 @@ class Templates {
|
|
|
4578
4606
|
}
|
|
4579
4607
|
}
|
|
4580
4608
|
|
|
4581
|
-
|
|
4582
|
-
|
|
4609
|
+
/**
|
|
4610
|
+
* Waits for the queued upgrades matching the filter, including the ones enqueued while
|
|
4611
|
+
* waiting: a component is only queued once its parent connects it, so a single pass
|
|
4612
|
+
* would miss everything nested.
|
|
4613
|
+
* @param {(el: Element) => boolean} accept
|
|
4614
|
+
*/
|
|
4615
|
+
const settle = async (accept) => {
|
|
4616
|
+
for (;;) {
|
|
4583
4617
|
const pending = Array.from(registry.upgrades)
|
|
4584
|
-
.filter(([child
|
|
4585
|
-
.map(([
|
|
4618
|
+
.filter(([child]) => accept(child))
|
|
4619
|
+
.map(([, promise]) => promise);
|
|
4620
|
+
if (pending.length === 0) {
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4586
4623
|
await Promise.all(pending);
|
|
4587
4624
|
}
|
|
4625
|
+
};
|
|
4626
|
+
|
|
4627
|
+
class Rendering {
|
|
4628
|
+
static async waitFor(el) {
|
|
4629
|
+
await settle((child) => el.contains(child));
|
|
4630
|
+
}
|
|
4588
4631
|
static async waitForChildren(el) {
|
|
4589
|
-
|
|
4590
|
-
.filter(([child, promise]) => el !== child && el.contains(child))
|
|
4591
|
-
.map(([child, promise]) => promise);
|
|
4592
|
-
await Promise.all(pending);
|
|
4632
|
+
await settle((child) => el !== child && el.contains(child));
|
|
4593
4633
|
}
|
|
4594
4634
|
}
|
|
4595
4635
|
|
|
@@ -4631,7 +4671,7 @@ class ParsedElement extends HTMLElement {
|
|
|
4631
4671
|
template(name) {
|
|
4632
4672
|
const { modules, data } = registry.context();
|
|
4633
4673
|
let t = this.#bits().TEMPLATES[name ?? 'default'].withData(data).withModules(modules);
|
|
4634
|
-
for (
|
|
4674
|
+
for (const k of ['l10n', 'config']) {
|
|
4635
4675
|
const v = this.constructor[k];
|
|
4636
4676
|
if (v) {
|
|
4637
4677
|
t = t.withOverlay({ [k]: v });
|
|
@@ -4661,6 +4701,19 @@ class ParsedElement extends HTMLElement {
|
|
|
4661
4701
|
return;
|
|
4662
4702
|
}
|
|
4663
4703
|
Reflect.set(this, 'disabled', disabled);
|
|
4704
|
+
if (disabled) {
|
|
4705
|
+
this.#unclaimFormDisabled();
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
//a disabled ancestor fieldset already matches the element through :disabled, and
|
|
4709
|
+
//an attribute of its own would keep the element disabled once the fieldset is
|
|
4710
|
+
//re-enabled, as formDisabledCallback(false) is only delivered on an actual state
|
|
4711
|
+
//change. a claim of its own made while already disabled by ancestry is safe: the
|
|
4712
|
+
//platform fires no callback for it, so it survives the fieldset being re-enabled
|
|
4713
|
+
#unclaimFormDisabled() {
|
|
4714
|
+
if (this.closest('fieldset:disabled')) {
|
|
4715
|
+
this.removeAttribute('disabled');
|
|
4716
|
+
}
|
|
4664
4717
|
}
|
|
4665
4718
|
async upgrade() {
|
|
4666
4719
|
if (this.#parsed) {
|
|
@@ -4676,6 +4729,9 @@ class ParsedElement extends HTMLElement {
|
|
|
4676
4729
|
);
|
|
4677
4730
|
const disabled = this.#disabledBeforeParsed ?? false;
|
|
4678
4731
|
await this.render({ slots, observed, disabled });
|
|
4732
|
+
if (disabled) {
|
|
4733
|
+
this.#unclaimFormDisabled();
|
|
4734
|
+
}
|
|
4679
4735
|
}
|
|
4680
4736
|
render(c) {}
|
|
4681
4737
|
reflect(fn) {
|
|
@@ -4772,13 +4828,22 @@ class Base64 {
|
|
|
4772
4828
|
|
|
4773
4829
|
let vi = 0;
|
|
4774
4830
|
let si = 0;
|
|
4775
|
-
|
|
4831
|
+
//every group yields three bytes, but a padded input stops short of the last
|
|
4832
|
+
//one or two: writing them anyway would rely on typed arrays dropping writes
|
|
4833
|
+
//past their length
|
|
4834
|
+
while (vi < nbytes) {
|
|
4776
4835
|
const v1 = d.indexOf(str.charAt(si++));
|
|
4777
4836
|
const v2 = d.indexOf(str.charAt(si++));
|
|
4778
4837
|
const v3 = d.indexOf(str.charAt(si++));
|
|
4779
4838
|
const v4 = d.indexOf(str.charAt(si++));
|
|
4780
4839
|
view[vi++] = (v1 << 2) | (v2 >> 4);
|
|
4840
|
+
if (vi === nbytes) {
|
|
4841
|
+
break;
|
|
4842
|
+
}
|
|
4781
4843
|
view[vi++] = ((v2 & 15) << 4) | (v3 >> 2);
|
|
4844
|
+
if (vi === nbytes) {
|
|
4845
|
+
break;
|
|
4846
|
+
}
|
|
4782
4847
|
view[vi++] = ((v3 & 3) << 6) | v4;
|
|
4783
4848
|
}
|
|
4784
4849
|
|
|
@@ -4805,7 +4870,7 @@ class Hex {
|
|
|
4805
4870
|
return Array.from(bytes)
|
|
4806
4871
|
.map((b) => b.toString(16))
|
|
4807
4872
|
.map((b) => (upper ? b.toUpperCase() : b))
|
|
4808
|
-
.map((o) => o.padStart(2, 0))
|
|
4873
|
+
.map((o) => o.padStart(2, '0'))
|
|
4809
4874
|
.join('');
|
|
4810
4875
|
}
|
|
4811
4876
|
}
|
|
@@ -5135,6 +5200,26 @@ const unmarshal = async (response, type) => {
|
|
|
5135
5200
|
}
|
|
5136
5201
|
};
|
|
5137
5202
|
|
|
5203
|
+
/**
|
|
5204
|
+
* Yields the entries of a headers or params initializer preserving nullish values:
|
|
5205
|
+
* normalizing through `Headers`/`URLSearchParams` first would stringify them to
|
|
5206
|
+
* "null"/"undefined" instead of removing the key.
|
|
5207
|
+
* @param {any} source
|
|
5208
|
+
* @returns {Iterable<[string, any]>}
|
|
5209
|
+
*/
|
|
5210
|
+
const rawEntries = (source) => {
|
|
5211
|
+
if (source == null) {
|
|
5212
|
+
return [];
|
|
5213
|
+
}
|
|
5214
|
+
if (typeof source === 'string') {
|
|
5215
|
+
return new URLSearchParams(source);
|
|
5216
|
+
}
|
|
5217
|
+
if (typeof source[Symbol.iterator] === 'function') {
|
|
5218
|
+
return source;
|
|
5219
|
+
}
|
|
5220
|
+
return Object.entries(source);
|
|
5221
|
+
};
|
|
5222
|
+
|
|
5138
5223
|
class HttpRequestBuilder {
|
|
5139
5224
|
#client;
|
|
5140
5225
|
#method;
|
|
@@ -5187,11 +5272,11 @@ class HttpRequestBuilder {
|
|
|
5187
5272
|
}
|
|
5188
5273
|
/**
|
|
5189
5274
|
* 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
|
|
5275
|
+
* @param {HeadersInit|Record<string,string|null|undefined>} hs
|
|
5191
5276
|
* @returns {HttpRequestBuilder} this builder
|
|
5192
5277
|
*/
|
|
5193
5278
|
headers(hs) {
|
|
5194
|
-
for (const [k, v] of
|
|
5279
|
+
for (const [k, v] of rawEntries(hs)) {
|
|
5195
5280
|
if (v == null) {
|
|
5196
5281
|
this.#headers.delete(k);
|
|
5197
5282
|
} else {
|
|
@@ -5216,11 +5301,11 @@ class HttpRequestBuilder {
|
|
|
5216
5301
|
}
|
|
5217
5302
|
/**
|
|
5218
5303
|
* 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
|
|
5304
|
+
* @param {URLSearchParams|Record<string,string|null|undefined>|string[][]|string} ps
|
|
5220
5305
|
* @returns {HttpRequestBuilder} this builder
|
|
5221
5306
|
*/
|
|
5222
5307
|
params(ps) {
|
|
5223
|
-
for (const [k, v] of
|
|
5308
|
+
for (const [k, v] of rawEntries(ps)) {
|
|
5224
5309
|
if (v == null) {
|
|
5225
5310
|
this.#params.delete(k);
|
|
5226
5311
|
} else {
|
|
@@ -5236,8 +5321,10 @@ class HttpRequestBuilder {
|
|
|
5236
5321
|
* @returns {HttpRequestBuilder} this builder
|
|
5237
5322
|
*/
|
|
5238
5323
|
param(k, ...vs) {
|
|
5324
|
+
//overriding, as header, headers and params all do: pass every value in one
|
|
5325
|
+
//call to get a multi valued parameter
|
|
5326
|
+
this.#params.delete(k);
|
|
5239
5327
|
if (vs.length === 0 || vs[0] == null) {
|
|
5240
|
-
this.#params.delete(k);
|
|
5241
5328
|
return this;
|
|
5242
5329
|
}
|
|
5243
5330
|
for (const v of vs) {
|
|
@@ -5270,7 +5357,7 @@ class HttpRequestBuilder {
|
|
|
5270
5357
|
/**
|
|
5271
5358
|
* Sets the request body as a FormData configured using the callback.
|
|
5272
5359
|
* `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
|
|
5273
|
-
* @param {
|
|
5360
|
+
* @param {(c: HttpMultipartRequestCustomizer) => void} callback
|
|
5274
5361
|
*/
|
|
5275
5362
|
multipart(callback) {
|
|
5276
5363
|
const formData = new FormData();
|
|
@@ -5435,7 +5522,7 @@ class HttpMultipartRequestCustomizer {
|
|
|
5435
5522
|
* @returns this builder
|
|
5436
5523
|
*/
|
|
5437
5524
|
blobs(name, values) {
|
|
5438
|
-
for (
|
|
5525
|
+
for (const v of values) {
|
|
5439
5526
|
this.#formData.append(name, v);
|
|
5440
5527
|
}
|
|
5441
5528
|
return this;
|
|
@@ -5470,7 +5557,16 @@ class LocalStorage extends Storage {
|
|
|
5470
5557
|
}
|
|
5471
5558
|
static load(k) {
|
|
5472
5559
|
const got = localStorage.getItem(k);
|
|
5473
|
-
|
|
5560
|
+
if (got === null) {
|
|
5561
|
+
return undefined;
|
|
5562
|
+
}
|
|
5563
|
+
try {
|
|
5564
|
+
return JSON.parse(got);
|
|
5565
|
+
} catch {
|
|
5566
|
+
//not what save wrote: drop it, otherwise every later read fails the same way
|
|
5567
|
+
localStorage.removeItem(k);
|
|
5568
|
+
return undefined;
|
|
5569
|
+
}
|
|
5474
5570
|
}
|
|
5475
5571
|
static remove(k) {
|
|
5476
5572
|
localStorage.removeItem(k);
|
|
@@ -5488,7 +5584,16 @@ class SessionStorage extends Storage {
|
|
|
5488
5584
|
}
|
|
5489
5585
|
static load(k) {
|
|
5490
5586
|
const got = sessionStorage.getItem(k);
|
|
5491
|
-
|
|
5587
|
+
if (got === null) {
|
|
5588
|
+
return undefined;
|
|
5589
|
+
}
|
|
5590
|
+
try {
|
|
5591
|
+
return JSON.parse(got);
|
|
5592
|
+
} catch {
|
|
5593
|
+
//not what save wrote: drop it, otherwise every later read fails the same way
|
|
5594
|
+
sessionStorage.removeItem(k);
|
|
5595
|
+
return undefined;
|
|
5596
|
+
}
|
|
5492
5597
|
}
|
|
5493
5598
|
static remove(k) {
|
|
5494
5599
|
sessionStorage.removeItem(k);
|
|
@@ -5510,7 +5615,7 @@ class VersionedLocalStorage {
|
|
|
5510
5615
|
return undefined;
|
|
5511
5616
|
}
|
|
5512
5617
|
if (stored.revision !== revision) {
|
|
5513
|
-
|
|
5618
|
+
LocalStorage.remove(key);
|
|
5514
5619
|
return undefined;
|
|
5515
5620
|
}
|
|
5516
5621
|
return stored.data;
|
|
@@ -5527,7 +5632,7 @@ class VersionedSessionStorage {
|
|
|
5527
5632
|
return undefined;
|
|
5528
5633
|
}
|
|
5529
5634
|
if (stored.revision !== revision) {
|
|
5530
|
-
|
|
5635
|
+
SessionStorage.remove(key);
|
|
5531
5636
|
return undefined;
|
|
5532
5637
|
}
|
|
5533
5638
|
return stored.data;
|
|
@@ -5662,7 +5767,7 @@ class Timing {
|
|
|
5662
5767
|
*/
|
|
5663
5768
|
static debounce(timeoutMs, func, options) {
|
|
5664
5769
|
const opts = options ?? Timing.DEBOUNCE_DEFAULT;
|
|
5665
|
-
let tid = null;
|
|
5770
|
+
let tid = /** @type {number | null} */ (null);
|
|
5666
5771
|
let args = [];
|
|
5667
5772
|
let previousTimestamp = 0;
|
|
5668
5773
|
|
|
@@ -5682,8 +5787,8 @@ class Timing {
|
|
|
5682
5787
|
}
|
|
5683
5788
|
};
|
|
5684
5789
|
|
|
5685
|
-
const debounced =
|
|
5686
|
-
args =
|
|
5790
|
+
const debounced = (...called) => {
|
|
5791
|
+
args = called;
|
|
5687
5792
|
previousTimestamp = performance.now();
|
|
5688
5793
|
if (tid === null) {
|
|
5689
5794
|
tid = setTimeout(later, timeoutMs);
|
|
@@ -5692,7 +5797,11 @@ class Timing {
|
|
|
5692
5797
|
}
|
|
5693
5798
|
}
|
|
5694
5799
|
};
|
|
5695
|
-
const abort = () =>
|
|
5800
|
+
const abort = () => {
|
|
5801
|
+
clearTimeout(tid ?? undefined);
|
|
5802
|
+
tid = null;
|
|
5803
|
+
args = [];
|
|
5804
|
+
};
|
|
5696
5805
|
return [debounced, abort];
|
|
5697
5806
|
}
|
|
5698
5807
|
static THROTTLE_DEFAULT = 0;
|
|
@@ -5707,7 +5816,7 @@ class Timing {
|
|
|
5707
5816
|
*/
|
|
5708
5817
|
static throttle(timeoutMs, func, options) {
|
|
5709
5818
|
const opts = options ?? Timing.THROTTLE_DEFAULT;
|
|
5710
|
-
let tid = null;
|
|
5819
|
+
let tid = /** @type {number | null} */ (null);
|
|
5711
5820
|
let args = [];
|
|
5712
5821
|
let previousTimestamp = 0;
|
|
5713
5822
|
|
|
@@ -5719,13 +5828,13 @@ class Timing {
|
|
|
5719
5828
|
args = [];
|
|
5720
5829
|
}
|
|
5721
5830
|
};
|
|
5722
|
-
const throttled =
|
|
5831
|
+
const throttled = (...called) => {
|
|
5723
5832
|
const now = performance.now();
|
|
5724
5833
|
if (!previousTimestamp && opts & Timing.THROTTLE_NO_LEADING) {
|
|
5725
5834
|
previousTimestamp = now;
|
|
5726
5835
|
}
|
|
5727
5836
|
const remaining = previousTimestamp === 0 ? 0 : timeoutMs - (now - previousTimestamp);
|
|
5728
|
-
args =
|
|
5837
|
+
args = called;
|
|
5729
5838
|
if (remaining <= 0 || remaining > timeoutMs) {
|
|
5730
5839
|
if (tid !== null) {
|
|
5731
5840
|
clearTimeout(tid);
|
|
@@ -5740,7 +5849,11 @@ class Timing {
|
|
|
5740
5849
|
tid = setTimeout(later, remaining);
|
|
5741
5850
|
}
|
|
5742
5851
|
};
|
|
5743
|
-
const abort = () =>
|
|
5852
|
+
const abort = () => {
|
|
5853
|
+
clearTimeout(tid ?? undefined);
|
|
5854
|
+
tid = null;
|
|
5855
|
+
args = [];
|
|
5856
|
+
};
|
|
5744
5857
|
return [throttled, abort];
|
|
5745
5858
|
}
|
|
5746
5859
|
}
|
|
@@ -5754,7 +5867,7 @@ class Bindings {
|
|
|
5754
5867
|
*/
|
|
5755
5868
|
static flatten(obj, prefix, stops) {
|
|
5756
5869
|
return Object.keys(obj).reduce((acc, k) => {
|
|
5757
|
-
const pre = prefix.length ? prefix
|
|
5870
|
+
const pre = prefix.length ? `${prefix}.${k}` : k;
|
|
5758
5871
|
if (!stops.has(pre) && typeof obj[k] === 'object' && obj[k] !== null) {
|
|
5759
5872
|
Object.assign(acc, Bindings.flatten(obj[k], pre, stops));
|
|
5760
5873
|
} else {
|
|
@@ -5772,7 +5885,7 @@ class Bindings {
|
|
|
5772
5885
|
static providePath(result, path, value) {
|
|
5773
5886
|
const keys = path.split('.').map((k) => (/^[0-9]+$/.test(k) ? +k : k));
|
|
5774
5887
|
let current = result ?? {};
|
|
5775
|
-
let previous = null;
|
|
5888
|
+
let previous = /** @type {any} */ (null);
|
|
5776
5889
|
for (let i = 0; ; ++i) {
|
|
5777
5890
|
const ckey = keys[i];
|
|
5778
5891
|
const pkey = keys[i - 1];
|
|
@@ -5805,12 +5918,12 @@ class Bindings {
|
|
|
5805
5918
|
if (!el.checked) {
|
|
5806
5919
|
return undefined;
|
|
5807
5920
|
}
|
|
5808
|
-
return el.dataset
|
|
5921
|
+
return el.dataset.fulBindType === 'boolean' ? el.value === 'true' : el.value;
|
|
5809
5922
|
}
|
|
5810
5923
|
if (el.getAttribute('type') === 'checkbox') {
|
|
5811
5924
|
return el.checked;
|
|
5812
5925
|
}
|
|
5813
|
-
if (el.dataset
|
|
5926
|
+
if (el.dataset.fulBindType === 'boolean') {
|
|
5814
5927
|
return !el.value ? null : el.value === 'true';
|
|
5815
5928
|
}
|
|
5816
5929
|
if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
|
|
@@ -5843,12 +5956,14 @@ class Bindings {
|
|
|
5843
5956
|
|
|
5844
5957
|
/**
|
|
5845
5958
|
*
|
|
5846
|
-
* @param {Element
|
|
5959
|
+
* @param {Element & {dataset?: any} & {checked?: boolean} & {value?: any}} el
|
|
5847
5960
|
* @returns
|
|
5848
5961
|
*/
|
|
5849
5962
|
static mutate(el, raw) {
|
|
5850
5963
|
if (el.getAttribute('type') === 'radio') {
|
|
5851
|
-
|
|
5964
|
+
//values are matched as strings, as ful-radio-group does: extract decodes
|
|
5965
|
+
//boolean radios, and payloads carry numbers where the attribute is text
|
|
5966
|
+
el.checked = raw != null && el.getAttribute('value') === String(raw);
|
|
5852
5967
|
return;
|
|
5853
5968
|
}
|
|
5854
5969
|
if (el.getAttribute('type') === 'checkbox') {
|
|
@@ -5872,7 +5987,9 @@ class Bindings {
|
|
|
5872
5987
|
static errors(form, es, scrollOnError) {
|
|
5873
5988
|
const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
|
|
5874
5989
|
const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
5875
|
-
form.querySelectorAll(`[name]`).forEach((el) =>
|
|
5990
|
+
form.querySelectorAll(`[name]`).forEach((el) => {
|
|
5991
|
+
el.setCustomValidity?.('');
|
|
5992
|
+
});
|
|
5876
5993
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
5877
5994
|
el.replaceChildren();
|
|
5878
5995
|
el.setAttribute('hidden', '');
|
|
@@ -5880,12 +5997,12 @@ class Bindings {
|
|
|
5880
5997
|
fieldErrors.forEach((e) => {
|
|
5881
5998
|
const name = e.context.replace(/\[/g, '.').replace(/\]\./g, '.').replace(/\]/g, '');
|
|
5882
5999
|
const parts = name.split('.');
|
|
5883
|
-
for (let i = parts.length; i
|
|
6000
|
+
for (let i = parts.length; i !== 0; --i) {
|
|
5884
6001
|
const prefix = parts.slice(0, i).join('.');
|
|
5885
6002
|
const suffix = parts.slice(i, parts.length).join('.');
|
|
5886
|
-
form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) =>
|
|
5887
|
-
input.setCustomValidity?.(e.reason, suffix)
|
|
5888
|
-
);
|
|
6003
|
+
form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) => {
|
|
6004
|
+
input.setCustomValidity?.(e.reason, suffix);
|
|
6005
|
+
});
|
|
5889
6006
|
}
|
|
5890
6007
|
});
|
|
5891
6008
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
@@ -5895,7 +6012,7 @@ class Bindings {
|
|
|
5895
6012
|
el.removeAttribute('hidden');
|
|
5896
6013
|
}
|
|
5897
6014
|
});
|
|
5898
|
-
if (es.length
|
|
6015
|
+
if (es.length === 0 || !scrollOnError) {
|
|
5899
6016
|
return;
|
|
5900
6017
|
}
|
|
5901
6018
|
Array.from(form.querySelectorAll(`:invalid`))
|
|
@@ -5967,7 +6084,8 @@ class FormLoader {
|
|
|
5967
6084
|
class Form extends ParsedElement {
|
|
5968
6085
|
form;
|
|
5969
6086
|
render() {
|
|
5970
|
-
const form =
|
|
6087
|
+
const form = document.createElement('form');
|
|
6088
|
+
this.form = form;
|
|
5971
6089
|
form.setAttribute('novalidate', '');
|
|
5972
6090
|
Attributes.forward('form-', this, form);
|
|
5973
6091
|
form.replaceChildren(...this.childNodes);
|
|
@@ -5990,50 +6108,53 @@ class Form extends ParsedElement {
|
|
|
5990
6108
|
*/
|
|
5991
6109
|
async submit(submitter) {
|
|
5992
6110
|
this.spinner(true);
|
|
6111
|
+
//one try: building the loader and preparing the request are as much part of a
|
|
6112
|
+
//submit as sending it, and a mapper that throws is how a caller reports a
|
|
6113
|
+
//problem with the values
|
|
6114
|
+
let values;
|
|
6115
|
+
let request;
|
|
5993
6116
|
try {
|
|
5994
6117
|
const loader = registry.component(this.getAttribute('loader') ?? 'loaders:form').create(this);
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
6118
|
+
values = Bindings.extractFrom(this.form, submitter);
|
|
6119
|
+
request = await loader.prepare(values, this);
|
|
6120
|
+
const se = new CustomEvent('submit', {
|
|
6121
|
+
bubbles: true,
|
|
6122
|
+
cancelable: true,
|
|
6123
|
+
detail: { submitter, values, request },
|
|
6124
|
+
});
|
|
6125
|
+
if (!this.dispatchEvent(se)) {
|
|
6126
|
+
return;
|
|
6127
|
+
}
|
|
6128
|
+
this.errors = [];
|
|
6129
|
+
const sre = new CustomEvent('submit:requested', {
|
|
6130
|
+
bubbles: true,
|
|
6131
|
+
cancelable: false,
|
|
6132
|
+
detail: { submitter, values: se.detail.values, request: se.detail.request },
|
|
6133
|
+
});
|
|
6134
|
+
let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
|
|
6135
|
+
request = sre.detail.request;
|
|
6136
|
+
|
|
6137
|
+
response = await loader.submit(request, this, response);
|
|
6138
|
+
const mapped = await loader.transform(response, this);
|
|
6139
|
+
this.dispatchEvent(
|
|
6140
|
+
new CustomEvent('submit:success', {
|
|
5999
6141
|
bubbles: true,
|
|
6000
|
-
cancelable:
|
|
6001
|
-
detail: { submitter, values, request },
|
|
6002
|
-
})
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
const sre = new CustomEvent('submit:requested', {
|
|
6142
|
+
cancelable: false,
|
|
6143
|
+
detail: { submitter, values, request, response: mapped },
|
|
6144
|
+
}),
|
|
6145
|
+
);
|
|
6146
|
+
} catch (e) {
|
|
6147
|
+
this.dispatchEvent(
|
|
6148
|
+
new CustomEvent('submit:failure', {
|
|
6008
6149
|
bubbles: true,
|
|
6009
6150
|
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);
|
|
6151
|
+
detail: { submitter, values, request, exception: e },
|
|
6152
|
+
}),
|
|
6153
|
+
);
|
|
6154
|
+
if (e instanceof Failure) {
|
|
6155
|
+
this.errors = e.problems;
|
|
6036
6156
|
}
|
|
6157
|
+
console.warn('failed to submit form', this, 'reason:', e);
|
|
6037
6158
|
} finally {
|
|
6038
6159
|
this.spinner(false);
|
|
6039
6160
|
}
|
|
@@ -6041,7 +6162,20 @@ class Form extends ParsedElement {
|
|
|
6041
6162
|
reset() {
|
|
6042
6163
|
this.form.reset();
|
|
6043
6164
|
}
|
|
6165
|
+
#spinning = 0;
|
|
6044
6166
|
spinner(spin) {
|
|
6167
|
+
//submits can overlap: only the outermost one saves and restores the button states
|
|
6168
|
+
if (spin) {
|
|
6169
|
+
++this.#spinning;
|
|
6170
|
+
if (this.#spinning !== 1) {
|
|
6171
|
+
return;
|
|
6172
|
+
}
|
|
6173
|
+
} else {
|
|
6174
|
+
this.#spinning = Math.max(0, this.#spinning - 1);
|
|
6175
|
+
if (this.#spinning !== 0) {
|
|
6176
|
+
return;
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6045
6179
|
this.querySelectorAll('ful-spinner').forEach((el) => {
|
|
6046
6180
|
const hel = /** @type HTMLElement */ (el);
|
|
6047
6181
|
hel.hidden = !spin;
|
|
@@ -6072,7 +6206,7 @@ class Form extends ParsedElement {
|
|
|
6072
6206
|
}
|
|
6073
6207
|
|
|
6074
6208
|
class Input extends ParsedElement {
|
|
6075
|
-
static observed = ['value', 'readonly:presence', 'required:presence'];
|
|
6209
|
+
static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
6076
6210
|
static slots = true;
|
|
6077
6211
|
static template = `
|
|
6078
6212
|
<div class="form-label">
|
|
@@ -6109,12 +6243,6 @@ class Input extends ParsedElement {
|
|
|
6109
6243
|
this._input = fragment.querySelector('input,textarea');
|
|
6110
6244
|
|
|
6111
6245
|
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
6246
|
this._input.addEventListener('keydown', (evt) => {
|
|
6119
6247
|
if (evt.key !== 'Enter' || this._type() === 'textarea') {
|
|
6120
6248
|
return;
|
|
@@ -6130,19 +6258,26 @@ class Input extends ParsedElement {
|
|
|
6130
6258
|
form.requestSubmit(submitter);
|
|
6131
6259
|
});
|
|
6132
6260
|
this._input.addEventListener('input', (evt) => {
|
|
6133
|
-
const
|
|
6134
|
-
if (!
|
|
6261
|
+
const mask = this.getAttribute('mask');
|
|
6262
|
+
if (!mask) {
|
|
6135
6263
|
return;
|
|
6136
6264
|
}
|
|
6265
|
+
const strip = (v) => v.replace(new RegExp(mask, 'g'), '');
|
|
6137
6266
|
const before = evt.target.value;
|
|
6138
|
-
const after = before
|
|
6267
|
+
const after = strip(before);
|
|
6139
6268
|
if (before === after) {
|
|
6140
6269
|
return;
|
|
6141
6270
|
}
|
|
6142
6271
|
const start = evt.target.selectionStart;
|
|
6143
|
-
const offset = before.length - after.length;
|
|
6144
6272
|
evt.target.value = after;
|
|
6145
|
-
|
|
6273
|
+
if (start === null) {
|
|
6274
|
+
//email, number and the date types have no selection to restore
|
|
6275
|
+
return;
|
|
6276
|
+
}
|
|
6277
|
+
//the caret keeps its place among the characters that survived, so only the
|
|
6278
|
+
//ones stripped before it count
|
|
6279
|
+
const caret = strip(before.slice(0, start)).length;
|
|
6280
|
+
evt.target.setSelectionRange(caret, caret);
|
|
6146
6281
|
});
|
|
6147
6282
|
this._input.addEventListener('change', (evt) => {
|
|
6148
6283
|
evt.stopPropagation();
|
|
@@ -6162,6 +6297,15 @@ class Input extends ParsedElement {
|
|
|
6162
6297
|
this._input.ariaDescribedByElements = [this._fieldError];
|
|
6163
6298
|
this._input.ariaLabelledByElements = [label];
|
|
6164
6299
|
this.replaceChildren(fragment);
|
|
6300
|
+
if (!skipObservedSetup) {
|
|
6301
|
+
// biome-ignore lint/complexity/noUselessThisAlias: keeps checkJs from seeing these as class fields
|
|
6302
|
+
const el = this;
|
|
6303
|
+
el.disabled = disabled;
|
|
6304
|
+
el.readonly = observed.readonly;
|
|
6305
|
+
el.required = observed.required;
|
|
6306
|
+
el.placeholder = observed.placeholder;
|
|
6307
|
+
el.value = observed.value;
|
|
6308
|
+
}
|
|
6165
6309
|
}
|
|
6166
6310
|
get value() {
|
|
6167
6311
|
const uppercase = this.hasAttribute('uppercase');
|
|
@@ -6188,6 +6332,11 @@ class Input extends ParsedElement {
|
|
|
6188
6332
|
}
|
|
6189
6333
|
set disabled(d) {
|
|
6190
6334
|
Attributes.toggle(this._input, 'disabled', d);
|
|
6335
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
6336
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
6337
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
6338
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
6339
|
+
Attributes.toggle(this, 'disabled', d);
|
|
6191
6340
|
}
|
|
6192
6341
|
get required() {
|
|
6193
6342
|
return this._input.getAttribute('aria-required') === 'true';
|
|
@@ -6198,6 +6347,18 @@ class Input extends ParsedElement {
|
|
|
6198
6347
|
Attributes.toggle(this, 'required', d);
|
|
6199
6348
|
});
|
|
6200
6349
|
}
|
|
6350
|
+
get placeholder() {
|
|
6351
|
+
const v = this._input.getAttribute('placeholder');
|
|
6352
|
+
return v === ' ' ? null : v;
|
|
6353
|
+
}
|
|
6354
|
+
set placeholder(d) {
|
|
6355
|
+
//without a placeholder :placeholder-shown never matches, and floating labels
|
|
6356
|
+
//rely on it, so a blank one stands in for none
|
|
6357
|
+
Attributes.set(this._input, 'placeholder', d ?? ' ');
|
|
6358
|
+
this.reflect(() => {
|
|
6359
|
+
Attributes.set(this, 'placeholder', d);
|
|
6360
|
+
});
|
|
6361
|
+
}
|
|
6201
6362
|
focus(options) {
|
|
6202
6363
|
this._input.focus(options);
|
|
6203
6364
|
}
|
|
@@ -6217,23 +6378,23 @@ class Input extends ParsedElement {
|
|
|
6217
6378
|
|
|
6218
6379
|
class LocalDate extends ParsedElement {
|
|
6219
6380
|
render() {
|
|
6220
|
-
const content = this.
|
|
6381
|
+
const content = this.textContent.trim();
|
|
6221
6382
|
if (content === '') {
|
|
6222
|
-
this.
|
|
6383
|
+
this.replaceChildren(this.getAttribute('default') ?? '');
|
|
6223
6384
|
return;
|
|
6224
6385
|
}
|
|
6225
6386
|
const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
6226
6387
|
const formatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' });
|
|
6227
6388
|
const [y, m, d] = content.split('-').map(Number);
|
|
6228
|
-
this.
|
|
6389
|
+
this.replaceChildren(formatter.format(new Date(y, m - 1, d)));
|
|
6229
6390
|
}
|
|
6230
6391
|
}
|
|
6231
6392
|
|
|
6232
6393
|
class Instant extends ParsedElement {
|
|
6233
6394
|
render() {
|
|
6234
|
-
const content = this.
|
|
6395
|
+
const content = this.textContent.trim();
|
|
6235
6396
|
if (content === '') {
|
|
6236
|
-
this.
|
|
6397
|
+
this.replaceChildren(this.getAttribute('default') ?? '');
|
|
6237
6398
|
return;
|
|
6238
6399
|
}
|
|
6239
6400
|
const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
@@ -6246,7 +6407,7 @@ class Instant extends ParsedElement {
|
|
|
6246
6407
|
second: 'numeric',
|
|
6247
6408
|
hour12: false,
|
|
6248
6409
|
});
|
|
6249
|
-
this.
|
|
6410
|
+
this.replaceChildren(format.format(new Date(Instant.isoToLocal(content))));
|
|
6250
6411
|
}
|
|
6251
6412
|
static isoToLocal(iso) {
|
|
6252
6413
|
//this is so sad
|
|
@@ -6259,16 +6420,17 @@ class Instant extends ParsedElement {
|
|
|
6259
6420
|
}
|
|
6260
6421
|
|
|
6261
6422
|
class InputLocalDate extends Input {
|
|
6262
|
-
static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
|
|
6423
|
+
static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
|
|
6263
6424
|
_type() {
|
|
6264
6425
|
return 'date';
|
|
6265
6426
|
}
|
|
6266
6427
|
render(conf) {
|
|
6267
6428
|
const { observed } = conf;
|
|
6268
6429
|
super.render(conf);
|
|
6430
|
+
//step first: on a time input min and max are snapped to its grid
|
|
6431
|
+
this.step = observed.step;
|
|
6269
6432
|
this.min = observed.min;
|
|
6270
6433
|
this.max = observed.max;
|
|
6271
|
-
this.step = observed.step;
|
|
6272
6434
|
}
|
|
6273
6435
|
get min() {
|
|
6274
6436
|
const v = this._input.min;
|
|
@@ -6314,13 +6476,14 @@ class InputLocalDate extends Input {
|
|
|
6314
6476
|
case 'd':
|
|
6315
6477
|
r.setDate(r.getDate() + offset * sign);
|
|
6316
6478
|
break;
|
|
6317
|
-
case 'm':
|
|
6479
|
+
case 'm': {
|
|
6318
6480
|
const originalDay = r.getDate();
|
|
6319
6481
|
r.setMonth(r.getMonth() + offset * sign);
|
|
6320
6482
|
if (r.getDate() !== originalDay) {
|
|
6321
6483
|
r.setDate(0);
|
|
6322
6484
|
}
|
|
6323
6485
|
break;
|
|
6486
|
+
}
|
|
6324
6487
|
case 'y':
|
|
6325
6488
|
r.setFullYear(r.getFullYear() + offset * sign);
|
|
6326
6489
|
break;
|
|
@@ -6333,10 +6496,62 @@ class InputLocalTime extends InputLocalDate {
|
|
|
6333
6496
|
_type() {
|
|
6334
6497
|
return 'time';
|
|
6335
6498
|
}
|
|
6499
|
+
get min() {
|
|
6500
|
+
const v = this._input.min;
|
|
6501
|
+
return v === '' ? null : v;
|
|
6502
|
+
}
|
|
6503
|
+
set min(v) {
|
|
6504
|
+
this._input.min = this.#fromNowOrOffset(v);
|
|
6505
|
+
}
|
|
6506
|
+
get max() {
|
|
6507
|
+
const v = this._input.max;
|
|
6508
|
+
return v === '' ? null : v;
|
|
6509
|
+
}
|
|
6510
|
+
set max(v) {
|
|
6511
|
+
this._input.max = this.#fromNowOrOffset(v);
|
|
6512
|
+
}
|
|
6513
|
+
/**
|
|
6514
|
+
* Resolves `now` and hour or minute offsets against the current time, wrapping
|
|
6515
|
+
* around midnight. `m` is minutes here, unlike the date offsets of the parent where
|
|
6516
|
+
* it is months: months mean nothing on a time. Anything else is passed through.
|
|
6517
|
+
*/
|
|
6518
|
+
#fromNowOrOffset(v) {
|
|
6519
|
+
if (!v) {
|
|
6520
|
+
return '';
|
|
6521
|
+
}
|
|
6522
|
+
const resolved = new Date();
|
|
6523
|
+
if (v !== 'now') {
|
|
6524
|
+
const re = /^([+-])(\d+)([hm])$/;
|
|
6525
|
+
const match = re.exec(v);
|
|
6526
|
+
if (!match) {
|
|
6527
|
+
return v;
|
|
6528
|
+
}
|
|
6529
|
+
const sign = match[1] === '-' ? -1 : 1;
|
|
6530
|
+
const offset = +match[2] * sign;
|
|
6531
|
+
if (match[3] === 'h') {
|
|
6532
|
+
resolved.setHours(resolved.getHours() + offset);
|
|
6533
|
+
} else {
|
|
6534
|
+
resolved.setMinutes(resolved.getMinutes() + offset);
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
return InputLocalTime.#snapped(resolved, Number(this._input.step) || 60);
|
|
6538
|
+
}
|
|
6539
|
+
/**
|
|
6540
|
+
* Truncates a time to the step grid: min anchors that grid, so a bound that is not
|
|
6541
|
+
* on it makes every value on it invalid.
|
|
6542
|
+
*/
|
|
6543
|
+
static #snapped(date, stepSeconds) {
|
|
6544
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
6545
|
+
const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
|
|
6546
|
+
const snapped = Math.floor(seconds / stepSeconds) * stepSeconds;
|
|
6547
|
+
const hh = pad(Math.floor(snapped / 3600));
|
|
6548
|
+
const mm = pad(Math.floor((snapped % 3600) / 60));
|
|
6549
|
+
return stepSeconds % 60 === 0 ? `${hh}:${mm}` : `${hh}:${mm}:${pad(snapped % 60)}`;
|
|
6550
|
+
}
|
|
6336
6551
|
}
|
|
6337
6552
|
|
|
6338
6553
|
class InputInstant extends Input {
|
|
6339
|
-
static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
|
|
6554
|
+
static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
|
|
6340
6555
|
_type() {
|
|
6341
6556
|
return 'datetime-local';
|
|
6342
6557
|
}
|
|
@@ -6381,28 +6596,28 @@ class InputFile extends Input {
|
|
|
6381
6596
|
static l10n = {
|
|
6382
6597
|
en: {
|
|
6383
6598
|
dropzonelabel: 'Click or drop your files here',
|
|
6384
|
-
|
|
6599
|
+
unacceptablefiletype: 'Only files of type {0} are supported',
|
|
6385
6600
|
maxfilesizeexceeded: 'Maximum supported file size is {0}',
|
|
6386
6601
|
maxtotalsizeexceeded: 'Maximum supported total file size is {0}',
|
|
6387
6602
|
maxfilesexceeded: 'Maximum files count exceeded',
|
|
6388
6603
|
},
|
|
6389
6604
|
it: {
|
|
6390
6605
|
dropzonelabel: 'Clicca o trascina i file qui',
|
|
6391
|
-
|
|
6606
|
+
unacceptablefiletype: 'Solo i file di tipo {0} sono supportati',
|
|
6392
6607
|
maxfilesizeexceeded: 'La dimensione massima di un file è di {0}',
|
|
6393
6608
|
maxtotalsizeexceeded: 'La dimensione massima complessiva dei file è di {0}',
|
|
6394
6609
|
maxfilesexceeded: 'Numero massimo di file superato',
|
|
6395
6610
|
},
|
|
6396
6611
|
es: {
|
|
6397
6612
|
dropzonelabel: 'Haz clic o arrastra tus archivos aquí',
|
|
6398
|
-
|
|
6613
|
+
unacceptablefiletype: 'Solo se admiten archivos de tipo {0}',
|
|
6399
6614
|
maxfilesizeexceeded: 'El tamaño máximo de archivo admitido es {0}',
|
|
6400
6615
|
maxtotalsizeexceeded: 'El tamaño total máximo admitido es {0}',
|
|
6401
6616
|
maxfilesexceeded: 'Se ha superado el número máximo de archivos',
|
|
6402
6617
|
},
|
|
6403
6618
|
fr: {
|
|
6404
6619
|
dropzonelabel: 'Cliquez ou déposez vos fichiers ici',
|
|
6405
|
-
|
|
6620
|
+
unacceptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
|
|
6406
6621
|
maxfilesizeexceeded: 'La taille maximale de fichier prise en charge est {0}',
|
|
6407
6622
|
maxtotalsizeexceeded: 'La taille totale maximale prise en charge est {0}',
|
|
6408
6623
|
maxfilesexceeded: 'Nombre maximal de fichiers dépassé',
|
|
@@ -6412,6 +6627,7 @@ class InputFile extends Input {
|
|
|
6412
6627
|
'value',
|
|
6413
6628
|
'readonly:presence',
|
|
6414
6629
|
'required:presence',
|
|
6630
|
+
'placeholder',
|
|
6415
6631
|
'accept:csv',
|
|
6416
6632
|
'multiple:presence',
|
|
6417
6633
|
'itemlist:presence',
|
|
@@ -6461,7 +6677,7 @@ class InputFile extends Input {
|
|
|
6461
6677
|
};
|
|
6462
6678
|
render(conf) {
|
|
6463
6679
|
const { observed } = conf;
|
|
6464
|
-
super.render(conf);
|
|
6680
|
+
super.render({ ...conf, skipObservedSetup: true });
|
|
6465
6681
|
this.#items = this.querySelector('ful-item-list');
|
|
6466
6682
|
this.#dropzone = this.querySelector('[data-ref=dropzone]');
|
|
6467
6683
|
this.#warnings = this.querySelector('ful-field-warnings');
|
|
@@ -6472,6 +6688,12 @@ class InputFile extends Input {
|
|
|
6472
6688
|
this.maxfiles = observed.maxfiles;
|
|
6473
6689
|
this.maxfilesize = observed.maxfilesize;
|
|
6474
6690
|
this.maxtotalsize = observed.maxtotalsize;
|
|
6691
|
+
|
|
6692
|
+
this.disabled = conf.disabled;
|
|
6693
|
+
this.readonly = observed.readonly;
|
|
6694
|
+
this.required = observed.required;
|
|
6695
|
+
this.placeholder = observed.placeholder;
|
|
6696
|
+
this.value = observed.value;
|
|
6475
6697
|
this.#warnings.addEventListener('animationend', (e) => {
|
|
6476
6698
|
e.target.remove();
|
|
6477
6699
|
});
|
|
@@ -6479,11 +6701,17 @@ class InputFile extends Input {
|
|
|
6479
6701
|
if (!e.target.closest('button')) {
|
|
6480
6702
|
return;
|
|
6481
6703
|
}
|
|
6482
|
-
const
|
|
6704
|
+
const idx = [...this.#items.children].indexOf(e.target.closest('ful-item'));
|
|
6705
|
+
if (idx === -1) {
|
|
6706
|
+
return;
|
|
6707
|
+
}
|
|
6483
6708
|
const dt = new DataTransfer();
|
|
6484
|
-
[...this.files]
|
|
6709
|
+
[...this.files]
|
|
6710
|
+
.filter((f, i) => i !== idx)
|
|
6711
|
+
.forEach((f) => {
|
|
6712
|
+
dt.items.add(f);
|
|
6713
|
+
});
|
|
6485
6714
|
this.files = dt.files;
|
|
6486
|
-
this.#update();
|
|
6487
6715
|
});
|
|
6488
6716
|
this.#dropzone.addEventListener('click', (e) => {
|
|
6489
6717
|
this.querySelector('input')?.click();
|
|
@@ -6494,10 +6722,15 @@ class InputFile extends Input {
|
|
|
6494
6722
|
});
|
|
6495
6723
|
this.#dropzone.addEventListener('drop', (e) => {
|
|
6496
6724
|
e.preventDefault();
|
|
6725
|
+
const dropped = [...e.dataTransfer.items].filter((i) => i.kind === 'file');
|
|
6726
|
+
if (dropped.length === 0) {
|
|
6727
|
+
return;
|
|
6728
|
+
}
|
|
6497
6729
|
const dt = new DataTransfer();
|
|
6498
|
-
|
|
6730
|
+
dropped.forEach((i) => {
|
|
6731
|
+
dt.items.add(i.getAsFile());
|
|
6732
|
+
});
|
|
6499
6733
|
this.files = dt.files;
|
|
6500
|
-
this.#update();
|
|
6501
6734
|
});
|
|
6502
6735
|
this._input.addEventListener('change', (e) => {
|
|
6503
6736
|
this.#update();
|
|
@@ -6512,6 +6745,7 @@ class InputFile extends Input {
|
|
|
6512
6745
|
}
|
|
6513
6746
|
#update() {
|
|
6514
6747
|
this.setCustomValidity();
|
|
6748
|
+
this.#warnings.replaceChildren();
|
|
6515
6749
|
this.#ensureAcceptable();
|
|
6516
6750
|
this.#ensureFileSizes();
|
|
6517
6751
|
this.#ensureTotalSize();
|
|
@@ -6522,7 +6756,7 @@ class InputFile extends Input {
|
|
|
6522
6756
|
.renderTo(this.#items);
|
|
6523
6757
|
}
|
|
6524
6758
|
warning(key, args) {
|
|
6525
|
-
this.template('warning').withOverlay({ key, args }).
|
|
6759
|
+
this.template('warning').withOverlay({ key, args }).appendTo(this.#warnings);
|
|
6526
6760
|
}
|
|
6527
6761
|
#ensureAcceptable() {
|
|
6528
6762
|
if (!this.#accept.length) {
|
|
@@ -6535,10 +6769,14 @@ class InputFile extends Input {
|
|
|
6535
6769
|
if (unacceptable.length === 0) {
|
|
6536
6770
|
return;
|
|
6537
6771
|
}
|
|
6538
|
-
this.warning('
|
|
6772
|
+
this.warning('unacceptablefiletype', this.#accept.join(', '));
|
|
6539
6773
|
const dt = new DataTransfer();
|
|
6540
|
-
[...this.files]
|
|
6541
|
-
|
|
6774
|
+
[...this.files]
|
|
6775
|
+
.filter((f) => !unacceptable.includes(f))
|
|
6776
|
+
.forEach((f) => {
|
|
6777
|
+
dt.items.add(f);
|
|
6778
|
+
});
|
|
6779
|
+
this._input.files = dt.files;
|
|
6542
6780
|
}
|
|
6543
6781
|
#ensureFilesCount() {
|
|
6544
6782
|
if (this.#maxfiles === null) {
|
|
@@ -6548,8 +6786,7 @@ class InputFile extends Input {
|
|
|
6548
6786
|
return;
|
|
6549
6787
|
}
|
|
6550
6788
|
this.warning('maxfilesexceeded');
|
|
6551
|
-
|
|
6552
|
-
this.files = dt.files;
|
|
6789
|
+
this._input.files = new DataTransfer().files;
|
|
6553
6790
|
}
|
|
6554
6791
|
|
|
6555
6792
|
#ensureFileSizes() {
|
|
@@ -6562,8 +6799,12 @@ class InputFile extends Input {
|
|
|
6562
6799
|
}
|
|
6563
6800
|
this.warning('maxfilesizeexceeded', this.#formatByteSize(this.#maxfilesize));
|
|
6564
6801
|
const dt = new DataTransfer();
|
|
6565
|
-
[...this.files]
|
|
6566
|
-
|
|
6802
|
+
[...this.files]
|
|
6803
|
+
.filter((f) => !oversized.includes(f))
|
|
6804
|
+
.forEach((f) => {
|
|
6805
|
+
dt.items.add(f);
|
|
6806
|
+
});
|
|
6807
|
+
this._input.files = dt.files;
|
|
6567
6808
|
}
|
|
6568
6809
|
#ensureTotalSize() {
|
|
6569
6810
|
if (this.#maxtotalsize === null) {
|
|
@@ -6574,7 +6815,7 @@ class InputFile extends Input {
|
|
|
6574
6815
|
return;
|
|
6575
6816
|
}
|
|
6576
6817
|
this.warning('maxtotalsizeexceeded', this.#formatByteSize(this.#maxtotalsize));
|
|
6577
|
-
this.files = new DataTransfer().files;
|
|
6818
|
+
this._input.files = new DataTransfer().files;
|
|
6578
6819
|
}
|
|
6579
6820
|
get accept() {
|
|
6580
6821
|
return this.#accept;
|
|
@@ -6600,6 +6841,7 @@ class InputFile extends Input {
|
|
|
6600
6841
|
}
|
|
6601
6842
|
set files(vs) {
|
|
6602
6843
|
this._input.files = vs;
|
|
6844
|
+
this.#update();
|
|
6603
6845
|
}
|
|
6604
6846
|
get file() {
|
|
6605
6847
|
return this.files[0] ?? null;
|
|
@@ -6620,7 +6862,6 @@ class InputFile extends Input {
|
|
|
6620
6862
|
return;
|
|
6621
6863
|
}
|
|
6622
6864
|
this.files = new DataTransfer().files;
|
|
6623
|
-
this.#update();
|
|
6624
6865
|
}
|
|
6625
6866
|
get totalsize() {
|
|
6626
6867
|
return Array.from(this.files).reduce((a, f) => a + f.size, 0);
|
|
@@ -6787,7 +7028,7 @@ class SelectLoader {
|
|
|
6787
7028
|
const http = registry.component('http-client');
|
|
6788
7029
|
const responseMapper = SelectLoader.#responseMapperFrom(el);
|
|
6789
7030
|
|
|
6790
|
-
if ('chunked'
|
|
7031
|
+
if ('chunked' === el.getAttribute('mode')) {
|
|
6791
7032
|
return new PartialRemoteLoader({
|
|
6792
7033
|
http,
|
|
6793
7034
|
url: el.getAttribute('src'),
|
|
@@ -6863,8 +7104,14 @@ class Dropdown extends ParsedElement {
|
|
|
6863
7104
|
});
|
|
6864
7105
|
this.replaceChildren(fragment);
|
|
6865
7106
|
}
|
|
7107
|
+
#selected() {
|
|
7108
|
+
return this.#menu?.querySelector('[selected]') ?? this.#menu?.firstElementChild ?? null;
|
|
7109
|
+
}
|
|
6866
7110
|
acceptSelection() {
|
|
6867
|
-
const selected = this.#
|
|
7111
|
+
const selected = this.#selected();
|
|
7112
|
+
if (!selected) {
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
6868
7115
|
this.#change(selected);
|
|
6869
7116
|
}
|
|
6870
7117
|
update(values) {
|
|
@@ -6900,6 +7147,9 @@ class Dropdown extends ParsedElement {
|
|
|
6900
7147
|
try {
|
|
6901
7148
|
const data = await loader();
|
|
6902
7149
|
this.update(data);
|
|
7150
|
+
} catch (e) {
|
|
7151
|
+
this.hide();
|
|
7152
|
+
throw e;
|
|
6903
7153
|
} finally {
|
|
6904
7154
|
this.#spinner.setAttribute('hidden', '');
|
|
6905
7155
|
this.#menu.removeAttribute('hidden');
|
|
@@ -6907,9 +7157,9 @@ class Dropdown extends ParsedElement {
|
|
|
6907
7157
|
}
|
|
6908
7158
|
async moveOrShow(forward, loader) {
|
|
6909
7159
|
if (this.shown) {
|
|
6910
|
-
const selected = this.#
|
|
6911
|
-
const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
6912
|
-
if (candidate) {
|
|
7160
|
+
const selected = this.#selected();
|
|
7161
|
+
const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
7162
|
+
if (selected && candidate) {
|
|
6913
7163
|
selected.removeAttribute('selected');
|
|
6914
7164
|
candidate.setAttribute('selected', '');
|
|
6915
7165
|
candidate.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
@@ -6962,6 +7212,7 @@ class Select extends ParsedElement {
|
|
|
6962
7212
|
#multiple;
|
|
6963
7213
|
#fieldError;
|
|
6964
7214
|
#values = new Map();
|
|
7215
|
+
#token = 0;
|
|
6965
7216
|
constructor() {
|
|
6966
7217
|
super();
|
|
6967
7218
|
this.internals = this.attachInternals();
|
|
@@ -6974,7 +7225,11 @@ class Select extends ParsedElement {
|
|
|
6974
7225
|
.create(this, { options: slots.options });
|
|
6975
7226
|
|
|
6976
7227
|
this.#multiple = this.hasAttribute('multiple');
|
|
6977
|
-
|
|
7228
|
+
try {
|
|
7229
|
+
await this.#loader.prefetch?.();
|
|
7230
|
+
} catch (/** @type any */ e) {
|
|
7231
|
+
console.warn('failed to prefetch select options', this, 'reason:', e);
|
|
7232
|
+
}
|
|
6978
7233
|
const fragment = this.template().withOverlay({ slots, name }).render();
|
|
6979
7234
|
this.#input = fragment.querySelector('input');
|
|
6980
7235
|
this.#items = fragment.querySelector('ful-item-list');
|
|
@@ -6993,11 +7248,9 @@ class Select extends ParsedElement {
|
|
|
6993
7248
|
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
6994
7249
|
this.#input.ariaDescribedByElements = [this.#fieldError];
|
|
6995
7250
|
this.#input.ariaLabelledByElements = [label];
|
|
6996
|
-
|
|
6997
|
-
const self = this;
|
|
6998
7251
|
const [dload, abortdload] = Timing.throttle(400, () => {
|
|
6999
|
-
|
|
7000
|
-
|
|
7252
|
+
this.#input.setAttribute('aria-expanded', 'true');
|
|
7253
|
+
this.#ddmenu.show(() => this.#loader.load(this.#input.value));
|
|
7001
7254
|
});
|
|
7002
7255
|
this.addEventListener('click', (/** @type any */ e) => {
|
|
7003
7256
|
if (e.target.matches('input')) {
|
|
@@ -7064,13 +7317,13 @@ class Select extends ParsedElement {
|
|
|
7064
7317
|
case 'ArrowUp': {
|
|
7065
7318
|
e.preventDefault();
|
|
7066
7319
|
this.#input.setAttribute('aria-expanded', 'true');
|
|
7067
|
-
this.#ddmenu.moveOrShow(false, () =>
|
|
7320
|
+
this.#ddmenu.moveOrShow(false, () => this.#loader.load(this.#input.value));
|
|
7068
7321
|
break;
|
|
7069
7322
|
}
|
|
7070
7323
|
case 'ArrowDown': {
|
|
7071
7324
|
e.preventDefault();
|
|
7072
7325
|
this.#input.setAttribute('aria-expanded', 'true');
|
|
7073
|
-
this.#ddmenu.moveOrShow(true, () =>
|
|
7326
|
+
this.#ddmenu.moveOrShow(true, () => this.#loader.load(this.#input.value));
|
|
7074
7327
|
break;
|
|
7075
7328
|
}
|
|
7076
7329
|
case 'Escape': {
|
|
@@ -7079,6 +7332,12 @@ class Select extends ParsedElement {
|
|
|
7079
7332
|
break;
|
|
7080
7333
|
}
|
|
7081
7334
|
case 'Enter': {
|
|
7335
|
+
if (!this.#ddmenu.shown) {
|
|
7336
|
+
//nothing to accept: submit the form as ful-input does. the inner
|
|
7337
|
+
//input carries form="" so it never submits one on its own
|
|
7338
|
+
this.#requestSubmit();
|
|
7339
|
+
return;
|
|
7340
|
+
}
|
|
7082
7341
|
e.preventDefault();
|
|
7083
7342
|
this.#input.setAttribute('aria-expanded', 'false');
|
|
7084
7343
|
this.#ddmenu.acceptSelection();
|
|
@@ -7127,6 +7386,16 @@ class Select extends ParsedElement {
|
|
|
7127
7386
|
async withLoader(fn) {
|
|
7128
7387
|
return await fn(this.#loader);
|
|
7129
7388
|
}
|
|
7389
|
+
#requestSubmit() {
|
|
7390
|
+
const form = this.internals.form;
|
|
7391
|
+
if (!form) {
|
|
7392
|
+
return;
|
|
7393
|
+
}
|
|
7394
|
+
const candidates = /** @type [HTMLButtonElement|HTMLInputElement] */ (
|
|
7395
|
+
Array.from(form.querySelectorAll('button:not(:disabled), input:not(:disabled)'))
|
|
7396
|
+
);
|
|
7397
|
+
form.requestSubmit(candidates.find((el) => el.type === 'submit'));
|
|
7398
|
+
}
|
|
7130
7399
|
#changed() {
|
|
7131
7400
|
const selection = [...this.#values.entries()].map((e) => ({
|
|
7132
7401
|
key: e[0],
|
|
@@ -7156,16 +7425,43 @@ class Select extends ParsedElement {
|
|
|
7156
7425
|
this.template('items').withOverlay({ entries: this.#values.entries() }).renderTo(this.#items);
|
|
7157
7426
|
}
|
|
7158
7427
|
set value(vs) {
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7428
|
+
//the csvm mapper yields [] for a missing multiple value, an empty string is
|
|
7429
|
+
//left alone: it is a usable key for an <option value="">
|
|
7430
|
+
const keys = vs == null ? [] : Array.isArray(vs) ? vs : [vs];
|
|
7431
|
+
//the keys are known synchronously and are all `value` reads, so they are applied
|
|
7432
|
+
//now: only the labels need the loader, until then a key stands in for its own
|
|
7433
|
+
this.#values = new Map(keys.map((k) => [k, [k]]));
|
|
7434
|
+
this.#syncBadges();
|
|
7435
|
+
const token = ++this.#token;
|
|
7436
|
+
if (keys.length === 0) {
|
|
7162
7437
|
return;
|
|
7163
7438
|
}
|
|
7164
|
-
(
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7439
|
+
this.#resolve(keys, token);
|
|
7440
|
+
}
|
|
7441
|
+
/**
|
|
7442
|
+
* Resolves the labels of the assigned keys. A failed lookup is left to reject so
|
|
7443
|
+
* that it is reported like any other failure: the keys stay applied either way.
|
|
7444
|
+
*/
|
|
7445
|
+
async #resolve(keys, token) {
|
|
7446
|
+
const entries = await this.#loader.exact(...keys);
|
|
7447
|
+
if (token !== this.#token) {
|
|
7448
|
+
//a newer assignment has been made in the meantime
|
|
7449
|
+
return;
|
|
7450
|
+
}
|
|
7451
|
+
//label the keys that are still selected: a removal made while the lookup was in
|
|
7452
|
+
//flight must not be undone by it, and a key the loader does not know is dropped
|
|
7453
|
+
const resolved = new Map(entries.map((e) => [e[0], e.slice(1)]));
|
|
7454
|
+
for (const key of keys) {
|
|
7455
|
+
if (!this.#values.has(key)) {
|
|
7456
|
+
continue;
|
|
7457
|
+
}
|
|
7458
|
+
if (resolved.has(key)) {
|
|
7459
|
+
this.#values.set(key, resolved.get(key));
|
|
7460
|
+
} else {
|
|
7461
|
+
this.#values.delete(key);
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
this.#syncBadges();
|
|
7169
7465
|
}
|
|
7170
7466
|
get value() {
|
|
7171
7467
|
if (this.#multiple) {
|
|
@@ -7184,6 +7480,11 @@ class Select extends ParsedElement {
|
|
|
7184
7480
|
}
|
|
7185
7481
|
set disabled(d) {
|
|
7186
7482
|
Attributes.toggle(this.#input, 'disabled', d);
|
|
7483
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
7484
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
7485
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
7486
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
7487
|
+
Attributes.toggle(this, 'disabled', d);
|
|
7187
7488
|
}
|
|
7188
7489
|
get readonly() {
|
|
7189
7490
|
return this.#input.readOnly;
|
|
@@ -7289,7 +7590,9 @@ class RadioGroup extends ParsedElement {
|
|
|
7289
7590
|
return [input, label];
|
|
7290
7591
|
});
|
|
7291
7592
|
|
|
7292
|
-
radioEls.forEach((el) =>
|
|
7593
|
+
radioEls.forEach((el) => {
|
|
7594
|
+
el.remove();
|
|
7595
|
+
});
|
|
7293
7596
|
this.template().withOverlay({ name, slots, inputsAndLabels }).renderTo(this);
|
|
7294
7597
|
this.#fieldset = this.firstElementChild;
|
|
7295
7598
|
this.disabled = disabled;
|
|
@@ -7333,6 +7636,11 @@ class RadioGroup extends ParsedElement {
|
|
|
7333
7636
|
}
|
|
7334
7637
|
set disabled(d) {
|
|
7335
7638
|
Attributes.toggle(this.#fieldset, 'disabled', d);
|
|
7639
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
7640
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
7641
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
7642
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
7643
|
+
Attributes.toggle(this, 'disabled', d);
|
|
7336
7644
|
}
|
|
7337
7645
|
get required() {
|
|
7338
7646
|
return this.#fieldset.getAttribute('aria-required') === 'true';
|
|
@@ -7382,7 +7690,7 @@ class Checkbox extends ParsedElement {
|
|
|
7382
7690
|
this.internals.role = 'presentation';
|
|
7383
7691
|
}
|
|
7384
7692
|
render({ slots, observed, disabled }) {
|
|
7385
|
-
const isSwitch = this.getAttribute('type')
|
|
7693
|
+
const isSwitch = this.getAttribute('type') === 'switch';
|
|
7386
7694
|
const klass = isSwitch ? 'form-check form-switch' : 'form-check';
|
|
7387
7695
|
const fragment = this.template().withOverlay({ slots, klass, isSwitch }).render();
|
|
7388
7696
|
this.#container = fragment.firstElementChild;
|
|
@@ -7446,6 +7754,11 @@ class Checkbox extends ParsedElement {
|
|
|
7446
7754
|
}
|
|
7447
7755
|
set disabled(d) {
|
|
7448
7756
|
Attributes.toggle(this.#input, 'disabled', d);
|
|
7757
|
+
//also on the host: a form associated element only matches :disabled through its
|
|
7758
|
+
//own attribute, and that is what keeps it out of the submitted values. no reflect
|
|
7759
|
+
//is needed, disabled is deliberately not observed: the platform delivers it
|
|
7760
|
+
//through formDisabledCallback, which also covers a disabled ancestor fieldset
|
|
7761
|
+
Attributes.toggle(this, 'disabled', d);
|
|
7449
7762
|
}
|
|
7450
7763
|
get required() {
|
|
7451
7764
|
return this.#input.getAttribute('aria-required') === 'true';
|
|
@@ -7486,9 +7799,10 @@ class Spinner extends ParsedElement {
|
|
|
7486
7799
|
class SortButton extends ParsedElement {
|
|
7487
7800
|
static observed = ['order'];
|
|
7488
7801
|
#order;
|
|
7489
|
-
render() {
|
|
7802
|
+
render({ observed }) {
|
|
7490
7803
|
const sorter = this.getAttribute('sorter');
|
|
7491
7804
|
const orders = ['asc', 'desc', null];
|
|
7805
|
+
this.order = observed.order;
|
|
7492
7806
|
this.addEventListener('click', () => {
|
|
7493
7807
|
const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
|
|
7494
7808
|
this.dispatchEvent(
|
|
@@ -7578,10 +7892,12 @@ class Pagination extends ParsedElement {
|
|
|
7578
7892
|
#total = 0;
|
|
7579
7893
|
#current = 0;
|
|
7580
7894
|
render({ observed }) {
|
|
7581
|
-
this.
|
|
7895
|
+
this.total = observed.total ?? 0;
|
|
7896
|
+
this.current = observed.current ?? 0;
|
|
7582
7897
|
this.addEventListener('click', (/** @type any */ evt) => {
|
|
7583
7898
|
const el = evt.target.closest('a');
|
|
7584
|
-
if (!el) {
|
|
7899
|
+
if (!el || el.classList.contains('disabled')) {
|
|
7900
|
+
//a disabled link leads nowhere: the page it would ask for does not exist
|
|
7585
7901
|
return;
|
|
7586
7902
|
}
|
|
7587
7903
|
this.dispatchEvent(
|
|
@@ -7597,25 +7913,20 @@ class Pagination extends ParsedElement {
|
|
|
7597
7913
|
}
|
|
7598
7914
|
update(current, total) {
|
|
7599
7915
|
const maxRender = Number(this.getAttribute('pages') ?? '5');
|
|
7600
|
-
const
|
|
7916
|
+
const hasPrev = current > 0;
|
|
7917
|
+
const hasNext = current + 1 < total;
|
|
7918
|
+
//a disabled arrow carries no page: there is nothing valid for it to point at
|
|
7919
|
+
const prev = { index: hasPrev ? current - 1 : null, enabled: hasPrev };
|
|
7601
7920
|
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
|
-
}
|
|
7921
|
+
const next = { index: hasNext ? current + 1 : null, enabled: hasNext };
|
|
7922
|
+
//the window holds at most maxRender pages, centered on the current one and slid
|
|
7923
|
+
//back towards the end so it stays full on the last pages
|
|
7924
|
+
const rendered = Math.max(1, Math.min(maxRender, total));
|
|
7925
|
+
const first = Math.max(0, Math.min(current - Math.floor((rendered - 1) / 2), total - rendered));
|
|
7926
|
+
const pages = Array.from({ length: rendered }, (_, offset) => ({
|
|
7927
|
+
index: first + offset,
|
|
7928
|
+
label: first + offset + 1,
|
|
7929
|
+
}));
|
|
7619
7930
|
this.template().withOverlay({ total, prev, curr, next, pages }).renderTo(this);
|
|
7620
7931
|
}
|
|
7621
7932
|
get total() {
|
|
@@ -7642,9 +7953,10 @@ class Pagination extends ParsedElement {
|
|
|
7642
7953
|
|
|
7643
7954
|
class TableSchemaParser {
|
|
7644
7955
|
static parse(nodeOrFragment, template) {
|
|
7645
|
-
|
|
7956
|
+
//nodeOrFragment is undefined when the slot is missing altogether
|
|
7957
|
+
const schema = nodeOrFragment ? Nodes.queryChildren(nodeOrFragment, 'schema') : null;
|
|
7646
7958
|
if (!schema) {
|
|
7647
|
-
throw new Error(
|
|
7959
|
+
throw new Error('missing expected <schema>: ful-table needs a <template slot="schema"> holding one');
|
|
7648
7960
|
}
|
|
7649
7961
|
const headersTr = document.createElement('tr');
|
|
7650
7962
|
const rowsTr = document.createElement('tr');
|
|
@@ -7863,9 +8175,10 @@ class Table extends ParsedElement {
|
|
|
7863
8175
|
this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
|
|
7864
8176
|
this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
|
|
7865
8177
|
this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
|
|
7866
|
-
this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
|
|
7867
8178
|
this.replaceChildren(fragment);
|
|
7868
|
-
|
|
8179
|
+
const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
|
|
8180
|
+
schema.headersTemplate.renderTo(thead);
|
|
8181
|
+
this.#sorters = thead.querySelectorAll('ful-sorter');
|
|
7869
8182
|
await Rendering.waitForChildren(this);
|
|
7870
8183
|
|
|
7871
8184
|
const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
|
|
@@ -7900,11 +8213,16 @@ class Table extends ParsedElement {
|
|
|
7900
8213
|
this.addEventListener('sort-requested', async (/** @type any */ e) => {
|
|
7901
8214
|
const sortRequest = e.detail.value.order ? e.detail.value : null;
|
|
7902
8215
|
await this.load(this.#latestRequest.pageRequest, sortRequest, this.#latestRequest.filterRequest);
|
|
7903
|
-
this.#sorters.forEach((s) =>
|
|
8216
|
+
this.#sorters.forEach((s) => {
|
|
8217
|
+
s.order = null;
|
|
8218
|
+
});
|
|
7904
8219
|
e.target.order = e.detail.value.order;
|
|
7905
8220
|
});
|
|
7906
8221
|
if (this.hasAttribute('autoload')) {
|
|
7907
|
-
|
|
8222
|
+
//not awaited: the first load must not hold up the upgrade, and a loader that
|
|
8223
|
+
//fails or never answers must not keep ftl:ready from firing for the page.
|
|
8224
|
+
//load renders its own error state and lets the failure reject, so it is reported
|
|
8225
|
+
this.reload();
|
|
7908
8226
|
}
|
|
7909
8227
|
}
|
|
7910
8228
|
|
|
@@ -7917,7 +8235,7 @@ class Table extends ParsedElement {
|
|
|
7917
8235
|
}
|
|
7918
8236
|
async load(pageRequest, sortRequest, filterRequest) {
|
|
7919
8237
|
this.#body.replaceChildren();
|
|
7920
|
-
this.#loading.removeAttribute('hidden'
|
|
8238
|
+
this.#loading.removeAttribute('hidden');
|
|
7921
8239
|
this.#feedback.setAttribute('hidden', '');
|
|
7922
8240
|
this.#noAutoload.setAttribute('hidden', '');
|
|
7923
8241
|
try {
|
|
@@ -7926,7 +8244,7 @@ class Table extends ParsedElement {
|
|
|
7926
8244
|
this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
|
|
7927
8245
|
} catch (/** @type any */ error) {
|
|
7928
8246
|
this.#loading.setAttribute('hidden', '');
|
|
7929
|
-
this.#feedback.removeAttribute('hidden'
|
|
8247
|
+
this.#feedback.removeAttribute('hidden');
|
|
7930
8248
|
if (!error.problems) {
|
|
7931
8249
|
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
|
|
7932
8250
|
} else {
|
|
@@ -7968,7 +8286,7 @@ class Table extends ParsedElement {
|
|
|
7968
8286
|
}
|
|
7969
8287
|
|
|
7970
8288
|
class InstantFilter extends Input {
|
|
7971
|
-
static observed = ['value:json', 'readonly:presence', 'required:presence'];
|
|
8289
|
+
static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
7972
8290
|
static template = `
|
|
7973
8291
|
<div class="form-label">
|
|
7974
8292
|
<label>{{{{ slots.default }}}}</label>
|
|
@@ -8002,10 +8320,16 @@ class InstantFilter extends Input {
|
|
|
8002
8320
|
this.#operator = this.querySelector('[data-ref=operator]');
|
|
8003
8321
|
this.#value1 = this.querySelector('[data-ref=value1]');
|
|
8004
8322
|
this.#value2 = this.querySelector('[data-ref=value2]');
|
|
8323
|
+
//Input.render only re-dispatches changes coming from the first operand
|
|
8324
|
+
this.#value2.addEventListener('change', (evt) => {
|
|
8325
|
+
evt.stopPropagation();
|
|
8326
|
+
this.#notifyChange();
|
|
8327
|
+
});
|
|
8005
8328
|
|
|
8006
8329
|
this.disabled = conf.disabled;
|
|
8007
8330
|
this.readonly = conf.observed.readonly;
|
|
8008
8331
|
this.required = conf.observed.required;
|
|
8332
|
+
this.placeholder = conf.observed.placeholder;
|
|
8009
8333
|
this.value = conf.observed.value;
|
|
8010
8334
|
|
|
8011
8335
|
this.addEventListener('click', (evt) => {
|
|
@@ -8015,9 +8339,13 @@ class InstantFilter extends Input {
|
|
|
8015
8339
|
}
|
|
8016
8340
|
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
8017
8341
|
const value = /** @type String */ (target.getAttribute('value'));
|
|
8342
|
+
const previous = btn.getAttribute('value');
|
|
8018
8343
|
Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
8019
8344
|
btn.setAttribute('value', value);
|
|
8020
8345
|
btn.innerHTML = target.innerHTML;
|
|
8346
|
+
if (previous !== value) {
|
|
8347
|
+
this.#notifyChange();
|
|
8348
|
+
}
|
|
8021
8349
|
});
|
|
8022
8350
|
}
|
|
8023
8351
|
|
|
@@ -8033,14 +8361,40 @@ class InstantFilter extends Input {
|
|
|
8033
8361
|
return;
|
|
8034
8362
|
}
|
|
8035
8363
|
const [operator, ...values] = v;
|
|
8036
|
-
this.#
|
|
8364
|
+
this.#showOperator(operator);
|
|
8037
8365
|
this.#value1.value = values[0] ? Instant.isoToLocal(values[0]) : values[0];
|
|
8038
8366
|
this.#value2.value = values[1] ? Instant.isoToLocal(values[1]) : values[1];
|
|
8039
8367
|
}
|
|
8368
|
+
#showOperator(operator) {
|
|
8369
|
+
this.#operator.setAttribute('value', operator);
|
|
8370
|
+
const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
|
|
8371
|
+
const item = items.find((a) => a.getAttribute('value') === operator);
|
|
8372
|
+
if (item) {
|
|
8373
|
+
this.#operator.innerHTML = item.innerHTML;
|
|
8374
|
+
}
|
|
8375
|
+
Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
|
|
8376
|
+
}
|
|
8377
|
+
#notifyChange() {
|
|
8378
|
+
this.dispatchEvent(
|
|
8379
|
+
new CustomEvent('change', {
|
|
8380
|
+
bubbles: true,
|
|
8381
|
+
cancelable: false,
|
|
8382
|
+
detail: {
|
|
8383
|
+
value: this.value,
|
|
8384
|
+
},
|
|
8385
|
+
}),
|
|
8386
|
+
);
|
|
8387
|
+
}
|
|
8388
|
+
get readonly() {
|
|
8389
|
+
return super.readonly;
|
|
8390
|
+
}
|
|
8040
8391
|
set readonly(v) {
|
|
8041
8392
|
this.#value2.readOnly = v;
|
|
8042
8393
|
super.readonly = v;
|
|
8043
8394
|
}
|
|
8395
|
+
get disabled() {
|
|
8396
|
+
return super.disabled;
|
|
8397
|
+
}
|
|
8044
8398
|
set disabled(d) {
|
|
8045
8399
|
Attributes.toggle(this.#value2, 'disabled', d);
|
|
8046
8400
|
super.disabled = d;
|
|
@@ -8048,7 +8402,7 @@ class InstantFilter extends Input {
|
|
|
8048
8402
|
}
|
|
8049
8403
|
|
|
8050
8404
|
class LocalDateFilter extends Input {
|
|
8051
|
-
static observed = ['value:json', 'readonly:presence', 'required:presence'];
|
|
8405
|
+
static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
8052
8406
|
static template = `
|
|
8053
8407
|
<div class="form-label">
|
|
8054
8408
|
<label>{{{{ slots.default }}}}</label>
|
|
@@ -8083,10 +8437,16 @@ class LocalDateFilter extends Input {
|
|
|
8083
8437
|
this.#operator = this.querySelector('[data-ref=operator]');
|
|
8084
8438
|
this.#value1 = this.querySelector('[data-ref=value1]');
|
|
8085
8439
|
this.#value2 = this.querySelector('[data-ref=value2]');
|
|
8440
|
+
//Input.render only re-dispatches changes coming from the first operand
|
|
8441
|
+
this.#value2.addEventListener('change', (evt) => {
|
|
8442
|
+
evt.stopPropagation();
|
|
8443
|
+
this.#notifyChange();
|
|
8444
|
+
});
|
|
8086
8445
|
|
|
8087
8446
|
this.disabled = conf.disabled;
|
|
8088
8447
|
this.readonly = conf.observed.readonly;
|
|
8089
8448
|
this.required = conf.observed.required;
|
|
8449
|
+
this.placeholder = conf.observed.placeholder;
|
|
8090
8450
|
this.value = conf.observed.value;
|
|
8091
8451
|
|
|
8092
8452
|
this.addEventListener('click', (evt) => {
|
|
@@ -8096,14 +8456,18 @@ class LocalDateFilter extends Input {
|
|
|
8096
8456
|
}
|
|
8097
8457
|
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
8098
8458
|
const value = /** @type String */ (target.getAttribute('value'));
|
|
8459
|
+
const previous = btn.getAttribute('value');
|
|
8099
8460
|
Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
8100
8461
|
btn.setAttribute('value', value);
|
|
8101
8462
|
btn.innerHTML = target.innerHTML;
|
|
8463
|
+
if (previous !== value) {
|
|
8464
|
+
this.#notifyChange();
|
|
8465
|
+
}
|
|
8102
8466
|
});
|
|
8103
8467
|
}
|
|
8104
8468
|
get value() {
|
|
8105
8469
|
const operator = this.#operator.getAttribute('value');
|
|
8106
|
-
const values = operator
|
|
8470
|
+
const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
|
|
8107
8471
|
return values.some((v) => v === '') ? undefined : [operator, ...values];
|
|
8108
8472
|
}
|
|
8109
8473
|
set value(v) {
|
|
@@ -8113,14 +8477,40 @@ class LocalDateFilter extends Input {
|
|
|
8113
8477
|
return;
|
|
8114
8478
|
}
|
|
8115
8479
|
const [operator, ...values] = v;
|
|
8116
|
-
this.#
|
|
8480
|
+
this.#showOperator(operator);
|
|
8117
8481
|
this.#value1.value = values[0];
|
|
8118
8482
|
this.#value2.value = values[1];
|
|
8119
8483
|
}
|
|
8484
|
+
#showOperator(operator) {
|
|
8485
|
+
this.#operator.setAttribute('value', operator);
|
|
8486
|
+
const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
|
|
8487
|
+
const item = items.find((a) => a.getAttribute('value') === operator);
|
|
8488
|
+
if (item) {
|
|
8489
|
+
this.#operator.innerHTML = item.innerHTML;
|
|
8490
|
+
}
|
|
8491
|
+
Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
|
|
8492
|
+
}
|
|
8493
|
+
#notifyChange() {
|
|
8494
|
+
this.dispatchEvent(
|
|
8495
|
+
new CustomEvent('change', {
|
|
8496
|
+
bubbles: true,
|
|
8497
|
+
cancelable: false,
|
|
8498
|
+
detail: {
|
|
8499
|
+
value: this.value,
|
|
8500
|
+
},
|
|
8501
|
+
}),
|
|
8502
|
+
);
|
|
8503
|
+
}
|
|
8504
|
+
get readonly() {
|
|
8505
|
+
return super.readonly;
|
|
8506
|
+
}
|
|
8120
8507
|
set readonly(v) {
|
|
8121
8508
|
this.#value2.readOnly = v;
|
|
8122
8509
|
super.readonly = v;
|
|
8123
8510
|
}
|
|
8511
|
+
get disabled() {
|
|
8512
|
+
return super.disabled;
|
|
8513
|
+
}
|
|
8124
8514
|
set disabled(d) {
|
|
8125
8515
|
Attributes.toggle(this.#value2, 'disabled', d);
|
|
8126
8516
|
super.disabled = d;
|
|
@@ -8128,7 +8518,7 @@ class LocalDateFilter extends Input {
|
|
|
8128
8518
|
}
|
|
8129
8519
|
|
|
8130
8520
|
class TextFilter extends Input {
|
|
8131
|
-
static observed = ['value:json', 'readonly:presence', 'required:presence'];
|
|
8521
|
+
static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
|
|
8132
8522
|
static template = `
|
|
8133
8523
|
<div class="form-label">
|
|
8134
8524
|
<label>{{{{ slots.default }}}}</label>
|
|
@@ -8152,6 +8542,8 @@ class TextFilter extends Input {
|
|
|
8152
8542
|
`;
|
|
8153
8543
|
#operator;
|
|
8154
8544
|
#value;
|
|
8545
|
+
//the sensitivity has no control of its own: it is carried through from whoever set the value
|
|
8546
|
+
#sensitivity = 'IGNORE_CASE';
|
|
8155
8547
|
render(conf) {
|
|
8156
8548
|
super.render({ ...conf, skipObservedSetup: true });
|
|
8157
8549
|
|
|
@@ -8161,6 +8553,7 @@ class TextFilter extends Input {
|
|
|
8161
8553
|
this.disabled = conf.disabled;
|
|
8162
8554
|
this.readonly = conf.observed.readonly;
|
|
8163
8555
|
this.required = conf.observed.required;
|
|
8556
|
+
this.placeholder = conf.observed.placeholder;
|
|
8164
8557
|
this.value = conf.observed.value;
|
|
8165
8558
|
|
|
8166
8559
|
this.addEventListener('click', (evt) => {
|
|
@@ -8170,13 +8563,17 @@ class TextFilter extends Input {
|
|
|
8170
8563
|
}
|
|
8171
8564
|
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
8172
8565
|
const value = /** @type String */ (target.getAttribute('value'));
|
|
8566
|
+
const previous = btn.getAttribute('value');
|
|
8173
8567
|
btn.setAttribute('value', value);
|
|
8174
8568
|
btn.innerHTML = target.innerHTML;
|
|
8569
|
+
if (previous !== value) {
|
|
8570
|
+
this.#notifyChange();
|
|
8571
|
+
}
|
|
8175
8572
|
});
|
|
8176
8573
|
}
|
|
8177
8574
|
get value() {
|
|
8178
8575
|
const operator = this.#operator.getAttribute('value');
|
|
8179
|
-
return this.#value.value === '' ? undefined : [operator,
|
|
8576
|
+
return this.#value.value === '' ? undefined : [operator, this.#sensitivity, this.#value.value];
|
|
8180
8577
|
}
|
|
8181
8578
|
set value(v) {
|
|
8182
8579
|
if (v == null) {
|
|
@@ -8184,15 +8581,35 @@ class TextFilter extends Input {
|
|
|
8184
8581
|
return;
|
|
8185
8582
|
}
|
|
8186
8583
|
const [operator, sensitivity, value] = v;
|
|
8187
|
-
this.#
|
|
8584
|
+
this.#showOperator(operator);
|
|
8585
|
+
this.#sensitivity = sensitivity ?? 'IGNORE_CASE';
|
|
8188
8586
|
this.#value.value = value;
|
|
8189
8587
|
}
|
|
8588
|
+
#showOperator(operator) {
|
|
8589
|
+
this.#operator.setAttribute('value', operator);
|
|
8590
|
+
const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
|
|
8591
|
+
const item = items.find((a) => a.getAttribute('value') === operator);
|
|
8592
|
+
if (item) {
|
|
8593
|
+
this.#operator.innerHTML = item.innerHTML;
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
#notifyChange() {
|
|
8597
|
+
this.dispatchEvent(
|
|
8598
|
+
new CustomEvent('change', {
|
|
8599
|
+
bubbles: true,
|
|
8600
|
+
cancelable: false,
|
|
8601
|
+
detail: {
|
|
8602
|
+
value: this.value,
|
|
8603
|
+
},
|
|
8604
|
+
}),
|
|
8605
|
+
);
|
|
8606
|
+
}
|
|
8190
8607
|
}
|
|
8191
8608
|
|
|
8192
8609
|
class LocalizationModule {
|
|
8193
8610
|
static t(k, ...args) {
|
|
8194
|
-
//@ts-
|
|
8195
|
-
const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.
|
|
8611
|
+
//@ts-expect-error l10n and language come from the element class and the data stack
|
|
8612
|
+
const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.en?.[k] ?? k;
|
|
8196
8613
|
if (args.length === 0) {
|
|
8197
8614
|
return format;
|
|
8198
8615
|
}
|
|
@@ -8275,9 +8692,11 @@ var ful = /*#__PURE__*/Object.freeze({
|
|
|
8275
8692
|
});
|
|
8276
8693
|
|
|
8277
8694
|
if (typeof window !== 'undefined') {
|
|
8695
|
+
// biome-ignore-start lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
|
|
8278
8696
|
window['ftl'] = ftl;
|
|
8279
8697
|
window['httpc'] = httpc;
|
|
8280
8698
|
window['ful'] = ful;
|
|
8699
|
+
// biome-ignore-end lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
|
|
8281
8700
|
}
|
|
8282
8701
|
|
|
8283
8702
|
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 };
|