@optionfactory/fml 9.0.0-rc14 → 9.0.0-rc15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +26 -0
- package/dist/fml.css +1 -0
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +215 -90
- 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/ful.css +1 -0
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +36 -6
- package/dist/ful.iife.js +214 -90
- 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 +214 -91
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +10 -0
- package/dist/web-types.json +17 -1
- package/package.json +1 -1
package/dist/ful.d.mts
CHANGED
|
@@ -664,8 +664,9 @@ declare class Anchors {
|
|
|
664
664
|
* start-aligned, clamped into the viewport; `flip` moves it above the
|
|
665
665
|
* anchor where the viewport leaves no room below, as the css
|
|
666
666
|
* `position-try-fallbacks` the paired popovers declare. A popover not
|
|
667
|
-
* already open is shown before
|
|
668
|
-
* sees is the right one
|
|
667
|
+
* already open is shown and placed before the call returns, so the first
|
|
668
|
+
* position anybody sees is the right one and the caller can move the focus
|
|
669
|
+
* into it on the next line.
|
|
669
670
|
*
|
|
670
671
|
* @param {HTMLElement} popover the `[popover]` element to show, or an
|
|
671
672
|
* already-shown element to place
|
|
@@ -1228,13 +1229,19 @@ declare class Dialog extends ParsedElement {
|
|
|
1228
1229
|
*
|
|
1229
1230
|
* The body is text or a node: a string is never read as markup, an
|
|
1230
1231
|
* `innerHTML` sink being a thing the library does not carry. Each button is
|
|
1231
|
-
* a `[result, label, className?]` tuple
|
|
1232
|
+
* a `[result, label, className?]` tuple rendered as its answer, or a node
|
|
1233
|
+
* the caller has built itself, which is how a button carries anything a
|
|
1234
|
+
* tuple cannot say and how a footer gets a spacer.
|
|
1232
1235
|
* @param {string} header
|
|
1233
1236
|
* @param {string|Node} body
|
|
1234
|
-
* @param {[string, string, string?][]} [buttons]
|
|
1237
|
+
* @param {([string, string, string?]|Node)[]} [buttons]
|
|
1238
|
+
* @param {{ className?: string }} [options] `className` dresses the dialog,
|
|
1239
|
+
* the way a declared `ful-dialog` is given one to size it
|
|
1235
1240
|
* @returns {Promise<DialogOutcome>}
|
|
1236
1241
|
*/
|
|
1237
|
-
static ask(header: string, body: string | Node, buttons?: [string, string, string?][]
|
|
1242
|
+
static ask(header: string, body: string | Node, buttons?: ([string, string, string?] | Node)[], { className }?: {
|
|
1243
|
+
className?: string;
|
|
1244
|
+
}): Promise<DialogOutcome>;
|
|
1238
1245
|
/**
|
|
1239
1246
|
* Asks a yes/no question, resolving with the confirm button's answer:
|
|
1240
1247
|
* the cancel button, the close button and Escape all answer false, a
|
|
@@ -1242,11 +1249,14 @@ declare class Dialog extends ParsedElement {
|
|
|
1242
1249
|
* @param {string} header
|
|
1243
1250
|
* @param {string|Node} body
|
|
1244
1251
|
* @param {{ confirm?: string, cancel?: string }} [labels] overriding the localized defaults
|
|
1252
|
+
* @param {{ className?: string }} [options] as `ask` takes them
|
|
1245
1253
|
* @returns {Promise<boolean>}
|
|
1246
1254
|
*/
|
|
1247
1255
|
static confirm(header: string, body: string | Node, labels?: {
|
|
1248
1256
|
confirm?: string;
|
|
1249
1257
|
cancel?: string;
|
|
1258
|
+
}, options?: {
|
|
1259
|
+
className?: string;
|
|
1250
1260
|
}): Promise<boolean>;
|
|
1251
1261
|
}
|
|
1252
1262
|
/**
|
|
@@ -1284,6 +1294,26 @@ declare class Drawer extends ParsedElement {
|
|
|
1284
1294
|
open(): void;
|
|
1285
1295
|
close(): void;
|
|
1286
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* A menu of commands on the native popover platform: the invoker named by
|
|
1299
|
+
* `for` toggles it and light dismiss and Escape come from the browser, while
|
|
1300
|
+
* the element adds the menu semantics and the keyboard protocol.
|
|
1301
|
+
*
|
|
1302
|
+
* The items are the buttons and links written inside it, kept as they are and
|
|
1303
|
+
* given their menu role and their place in the roving focus. Activation is the
|
|
1304
|
+
* item's own click, which the element only follows by closing.
|
|
1305
|
+
*/
|
|
1306
|
+
declare class Menu extends ParsedElement {
|
|
1307
|
+
#private;
|
|
1308
|
+
static attributes: string[];
|
|
1309
|
+
render(): void;
|
|
1310
|
+
/**
|
|
1311
|
+
* The commands, in the order the arrows walk them: the menu's own buttons
|
|
1312
|
+
* and links, minus the disabled and hidden ones, which keep their role and
|
|
1313
|
+
* leave the walk.
|
|
1314
|
+
*/
|
|
1315
|
+
get items(): HTMLElement[];
|
|
1316
|
+
}
|
|
1287
1317
|
/** A transient feedback region: each show() stacks a toast that retires on its own timer. */
|
|
1288
1318
|
declare class Toasts extends ParsedElement {
|
|
1289
1319
|
#private;
|
|
@@ -1412,6 +1442,6 @@ declare class Plugin {
|
|
|
1412
1442
|
});
|
|
1413
1443
|
configure(registry: any): void;
|
|
1414
1444
|
}
|
|
1415
|
-
export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, NumberFilter, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, TableSchemaParser, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
|
|
1445
|
+
export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, Menu, NumberFilter, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, TableSchemaParser, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
|
|
1416
1446
|
|
|
1417
1447
|
export as namespace ful;
|
package/dist/ful.iife.js
CHANGED
|
@@ -2304,7 +2304,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2304
2304
|
|
|
2305
2305
|
const showOncePlaced = (popover, anchored) => {
|
|
2306
2306
|
popover.style.visibility = 'hidden';
|
|
2307
|
-
|
|
2307
|
+
queueMicrotask(() => {
|
|
2308
2308
|
if (popover.matches(':popover-open')) {
|
|
2309
2309
|
place(popover, anchored);
|
|
2310
2310
|
}
|
|
@@ -2443,8 +2443,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2443
2443
|
* start-aligned, clamped into the viewport; `flip` moves it above the
|
|
2444
2444
|
* anchor where the viewport leaves no room below, as the css
|
|
2445
2445
|
* `position-try-fallbacks` the paired popovers declare. A popover not
|
|
2446
|
-
* already open is shown before
|
|
2447
|
-
* sees is the right one
|
|
2446
|
+
* already open is shown and placed before the call returns, so the first
|
|
2447
|
+
* position anybody sees is the right one and the caller can move the focus
|
|
2448
|
+
* into it on the next line.
|
|
2448
2449
|
*
|
|
2449
2450
|
* @param {HTMLElement} popover the `[popover]` element to show, or an
|
|
2450
2451
|
* already-shown element to place
|
|
@@ -2470,16 +2471,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2470
2471
|
popover.style.left = `${clamp(box.left, PAD, Math.max(PAD, viewport.clientWidth - here.width - PAD))}px`;
|
|
2471
2472
|
popover.style.top = `${clamp(top, PAD, Math.max(PAD, viewport.clientHeight - here.height - PAD))}px`;
|
|
2472
2473
|
};
|
|
2473
|
-
if (popover.matches(':popover-open')
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
popover.style.visibility = 'hidden';
|
|
2478
|
-
popover.showPopover();
|
|
2479
|
-
requestAnimationFrame(() => {
|
|
2480
|
-
placeShown();
|
|
2481
|
-
popover.style.removeProperty('visibility');
|
|
2482
|
-
});
|
|
2474
|
+
if (typeof popover.showPopover === 'function' && !popover.matches(':popover-open')) {
|
|
2475
|
+
popover.showPopover();
|
|
2476
|
+
}
|
|
2477
|
+
placeShown();
|
|
2483
2478
|
return popover;
|
|
2484
2479
|
}
|
|
2485
2480
|
}
|
|
@@ -2771,6 +2766,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2771
2766
|
//from the host when it gave one, since it has to set aria-controls before
|
|
2772
2767
|
//this element upgrades
|
|
2773
2768
|
this.#menu.id = this.declared('listbox') || index_mjs.Attributes.uid('ful-listbox');
|
|
2769
|
+
this.#menu.addEventListener('mousedown', (evt) => {
|
|
2770
|
+
if (evt.target.closest('li')) {
|
|
2771
|
+
evt.preventDefault();
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
2774
|
this.#menu.addEventListener('click', (evt) => {
|
|
2775
2775
|
evt.stopPropagation();
|
|
2776
2776
|
const li = evt.target.closest('li');
|
|
@@ -2778,7 +2778,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2778
2778
|
this.hide();
|
|
2779
2779
|
return;
|
|
2780
2780
|
}
|
|
2781
|
-
if (li.
|
|
2781
|
+
if (li.matches('[aria-disabled="true"]')) {
|
|
2782
2782
|
return;
|
|
2783
2783
|
}
|
|
2784
2784
|
this.#change(li);
|
|
@@ -2787,8 +2787,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2787
2787
|
}
|
|
2788
2788
|
#selected() {
|
|
2789
2789
|
return (
|
|
2790
|
-
this.#menu?.querySelector('[selected]:not([aria-disabled])') ??
|
|
2791
|
-
this.#menu?.querySelector('li:not([aria-disabled])') ??
|
|
2790
|
+
this.#menu?.querySelector('[selected]:not([aria-disabled="true"])') ??
|
|
2791
|
+
this.#menu?.querySelector('li:not([aria-disabled="true"])') ??
|
|
2792
2792
|
null
|
|
2793
2793
|
);
|
|
2794
2794
|
}
|
|
@@ -2830,7 +2830,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2830
2830
|
li.setAttribute('aria-selected', picked ? 'true' : 'false');
|
|
2831
2831
|
//metadata.disabled refuses the entry, metadata.reason saying why
|
|
2832
2832
|
const disabled = !!entry?.metadata?.disabled;
|
|
2833
|
-
|
|
2833
|
+
if (disabled) {
|
|
2834
|
+
li.setAttribute('aria-disabled', 'true');
|
|
2835
|
+
} else {
|
|
2836
|
+
li.removeAttribute('aria-disabled');
|
|
2837
|
+
}
|
|
2834
2838
|
if (disabled && entry.metadata.reason) {
|
|
2835
2839
|
li.setAttribute('title', String(entry.metadata.reason));
|
|
2836
2840
|
} else {
|
|
@@ -2919,7 +2923,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2919
2923
|
}
|
|
2920
2924
|
#walk(from, forward) {
|
|
2921
2925
|
for (let li = from; li; li = li[`${forward ? 'next' : 'previous'}ElementSibling`]) {
|
|
2922
|
-
if (!li.
|
|
2926
|
+
if (!li.matches('[aria-disabled="true"]')) {
|
|
2923
2927
|
return li;
|
|
2924
2928
|
}
|
|
2925
2929
|
}
|
|
@@ -4363,15 +4367,110 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4363
4367
|
}
|
|
4364
4368
|
}
|
|
4365
4369
|
|
|
4370
|
+
/** how long a type-ahead buffer stands before the next character starts a new search */
|
|
4371
|
+
const TYPEAHEAD_WINDOW = 500;
|
|
4372
|
+
|
|
4373
|
+
/**
|
|
4374
|
+
* The keyboard protocol of a menu popover: the focus moving into the menu as it
|
|
4375
|
+
* opens and back to the invoker as it closes, roving focus over the items with
|
|
4376
|
+
* the arrows, Home and End, type-ahead over their text, and Enter and Space
|
|
4377
|
+
* activating the focused one.
|
|
4378
|
+
*
|
|
4379
|
+
* The menu owns what an item is: `items` answers them in the order they are
|
|
4380
|
+
* walked, read on every gesture so a menu that fills itself later needs no
|
|
4381
|
+
* rewiring, and `current` names the one an opening focuses, the first otherwise.
|
|
4382
|
+
*
|
|
4383
|
+
* @param {HTMLElement} invoker the element the menu belongs to, and where the focus goes back
|
|
4384
|
+
* @param {HTMLElement} menu the `[popover]` holding the items
|
|
4385
|
+
* @param {{items: () => HTMLElement[], current?: () => HTMLElement|null|undefined}} conf
|
|
4386
|
+
*/
|
|
4387
|
+
const wireMenuKeys = (invoker, menu, { items, current = () => null }) => {
|
|
4388
|
+
let typed = '';
|
|
4389
|
+
let typedAt = 0;
|
|
4390
|
+
menu.addEventListener('toggle', (/** @type any */ evt) => {
|
|
4391
|
+
if (evt.newState !== 'open') {
|
|
4392
|
+
//give the invoker back the focus the menu had borrowed, without
|
|
4393
|
+
//stealing it from wherever else the close came from
|
|
4394
|
+
if (menu.contains(document.activeElement)) {
|
|
4395
|
+
invoker.focus();
|
|
4396
|
+
}
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
const all = items();
|
|
4400
|
+
(current() ?? all[0])?.focus();
|
|
4401
|
+
});
|
|
4402
|
+
menu.addEventListener('keydown', (evt) => {
|
|
4403
|
+
const target = /** @type HTMLElement */ (evt.target);
|
|
4404
|
+
const all = items();
|
|
4405
|
+
const item = all.find((candidate) => candidate === target || candidate.contains(target));
|
|
4406
|
+
if (!item) {
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
const at = all.indexOf(item);
|
|
4410
|
+
switch (evt.code) {
|
|
4411
|
+
case 'ArrowDown': {
|
|
4412
|
+
evt.preventDefault();
|
|
4413
|
+
all[(at + 1) % all.length]?.focus();
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
case 'ArrowUp': {
|
|
4417
|
+
evt.preventDefault();
|
|
4418
|
+
all[(at - 1 + all.length) % all.length]?.focus();
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
case 'Home': {
|
|
4422
|
+
evt.preventDefault();
|
|
4423
|
+
all[0]?.focus();
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
case 'End': {
|
|
4427
|
+
evt.preventDefault();
|
|
4428
|
+
all[all.length - 1]?.focus();
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
case 'Enter':
|
|
4432
|
+
case 'Space': {
|
|
4433
|
+
evt.preventDefault();
|
|
4434
|
+
item.click();
|
|
4435
|
+
invoker.focus();
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4438
|
+
case 'Escape': {
|
|
4439
|
+
//the platform's close request hides the menu, the focus is placed
|
|
4440
|
+
//on the invoker before the focused item is detached from it
|
|
4441
|
+
invoker.focus();
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
if (evt.key.length !== 1 || evt.ctrlKey || evt.altKey || evt.metaKey) {
|
|
4446
|
+
return;
|
|
4447
|
+
}
|
|
4448
|
+
const now = Date.now();
|
|
4449
|
+
typed = now - typedAt > TYPEAHEAD_WINDOW ? evt.key : typed + evt.key;
|
|
4450
|
+
typedAt = now;
|
|
4451
|
+
const wanted = typed.toLowerCase();
|
|
4452
|
+
const repeated = [...wanted].every((character) => character === wanted[0]);
|
|
4453
|
+
const search = repeated ? wanted[0] : wanted;
|
|
4454
|
+
const from = repeated ? at + 1 : at;
|
|
4455
|
+
const found = [...all.slice(from), ...all.slice(0, from)].find((candidate) =>
|
|
4456
|
+
(candidate.textContent ?? '').trim().toLowerCase().startsWith(search),
|
|
4457
|
+
);
|
|
4458
|
+
if (found) {
|
|
4459
|
+
evt.preventDefault();
|
|
4460
|
+
found.focus();
|
|
4461
|
+
}
|
|
4462
|
+
});
|
|
4463
|
+
};
|
|
4464
|
+
|
|
4366
4465
|
/**
|
|
4367
4466
|
* An invoker button paired with the `ul[popover][role=menu]` that follows it:
|
|
4368
4467
|
* the chrome behind every filter's operator, sensitivity and boolean value.
|
|
4369
4468
|
*
|
|
4370
4469
|
* It fills the menu from a vocabulary, wires it the first time more than one
|
|
4371
4470
|
* choice survives the whitelist, pins the button to a static glyph when a
|
|
4372
|
-
* single one does,
|
|
4373
|
-
*
|
|
4374
|
-
*
|
|
4471
|
+
* single one does, takes the shared menu keyboard protocol, and keeps the
|
|
4472
|
+
* button's value, glyph and aria-label in step. A pick that changes the value
|
|
4473
|
+
* calls back; the host decides what that means.
|
|
4375
4474
|
*
|
|
4376
4475
|
* The button's `value` attribute is the store, as it is for a native control:
|
|
4377
4476
|
* the menu protocol finds the current item by it, and nothing mirrors it.
|
|
@@ -4529,64 +4628,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4529
4628
|
return Array.from(this.#menu.querySelectorAll('li > a'), (a) => /** @type HTMLAnchorElement */ (a));
|
|
4530
4629
|
}
|
|
4531
4630
|
#wire() {
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
if (evt.newState !== 'open') {
|
|
4537
|
-
//give the invoker back the focus the menu had borrowed, without
|
|
4538
|
-
//stealing it from wherever else the close came from
|
|
4539
|
-
if (menu.contains(document.activeElement)) {
|
|
4540
|
-
button.focus();
|
|
4541
|
-
}
|
|
4542
|
-
return;
|
|
4543
|
-
}
|
|
4544
|
-
const items = this.#items();
|
|
4545
|
-
(items.find((a) => a.getAttribute('value') === this.value) ?? items[0])?.focus();
|
|
4546
|
-
});
|
|
4547
|
-
menu.addEventListener('keydown', (evt) => {
|
|
4548
|
-
const target = /** @type HTMLElement */ (evt.target);
|
|
4549
|
-
const item = /** @type HTMLAnchorElement | null */ (target.closest('li > a'));
|
|
4550
|
-
if (!item) {
|
|
4551
|
-
return;
|
|
4552
|
-
}
|
|
4553
|
-
const items = this.#items();
|
|
4554
|
-
const at = items.indexOf(item);
|
|
4555
|
-
switch (evt.code) {
|
|
4556
|
-
case 'ArrowDown': {
|
|
4557
|
-
evt.preventDefault();
|
|
4558
|
-
items[(at + 1) % items.length]?.focus();
|
|
4559
|
-
break;
|
|
4560
|
-
}
|
|
4561
|
-
case 'ArrowUp': {
|
|
4562
|
-
evt.preventDefault();
|
|
4563
|
-
items[(at - 1 + items.length) % items.length]?.focus();
|
|
4564
|
-
break;
|
|
4565
|
-
}
|
|
4566
|
-
case 'Home': {
|
|
4567
|
-
evt.preventDefault();
|
|
4568
|
-
items[0]?.focus();
|
|
4569
|
-
break;
|
|
4570
|
-
}
|
|
4571
|
-
case 'End': {
|
|
4572
|
-
evt.preventDefault();
|
|
4573
|
-
items[items.length - 1]?.focus();
|
|
4574
|
-
break;
|
|
4575
|
-
}
|
|
4576
|
-
case 'Enter':
|
|
4577
|
-
case 'Space': {
|
|
4578
|
-
evt.preventDefault();
|
|
4579
|
-
item.click();
|
|
4580
|
-
button.focus();
|
|
4581
|
-
break;
|
|
4582
|
-
}
|
|
4583
|
-
case 'Escape': {
|
|
4584
|
-
//the platform's close request hides the menu, the focus is placed
|
|
4585
|
-
//on the invoker before the focused item is detached from it
|
|
4586
|
-
button.focus();
|
|
4587
|
-
break;
|
|
4588
|
-
}
|
|
4589
|
-
}
|
|
4631
|
+
Anchors.wire(this.#button, this.#menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
|
|
4632
|
+
wireMenuKeys(this.#button, this.#menu, {
|
|
4633
|
+
items: () => this.#items(),
|
|
4634
|
+
current: () => this.#items().find((a) => a.getAttribute('value') === this.value),
|
|
4590
4635
|
});
|
|
4591
4636
|
}
|
|
4592
4637
|
}
|
|
@@ -5561,15 +5606,22 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5561
5606
|
*
|
|
5562
5607
|
* The body is text or a node: a string is never read as markup, an
|
|
5563
5608
|
* `innerHTML` sink being a thing the library does not carry. Each button is
|
|
5564
|
-
* a `[result, label, className?]` tuple
|
|
5609
|
+
* a `[result, label, className?]` tuple rendered as its answer, or a node
|
|
5610
|
+
* the caller has built itself, which is how a button carries anything a
|
|
5611
|
+
* tuple cannot say and how a footer gets a spacer.
|
|
5565
5612
|
* @param {string} header
|
|
5566
5613
|
* @param {string|Node} body
|
|
5567
|
-
* @param {[string, string, string?][]} [buttons]
|
|
5614
|
+
* @param {([string, string, string?]|Node)[]} [buttons]
|
|
5615
|
+
* @param {{ className?: string }} [options] `className` dresses the dialog,
|
|
5616
|
+
* the way a declared `ful-dialog` is given one to size it
|
|
5568
5617
|
* @returns {Promise<DialogOutcome>}
|
|
5569
5618
|
*/
|
|
5570
|
-
static async ask(header, body, buttons = []) {
|
|
5619
|
+
static async ask(header, body, buttons = [], { className } = {}) {
|
|
5571
5620
|
const dialog = document.createElement('ful-dialog');
|
|
5572
5621
|
dialog.setAttribute('header', header);
|
|
5622
|
+
if (className) {
|
|
5623
|
+
dialog.className = className;
|
|
5624
|
+
}
|
|
5573
5625
|
if (typeof body === 'string') {
|
|
5574
5626
|
dialog.textContent = body;
|
|
5575
5627
|
} else if (body) {
|
|
@@ -5580,12 +5632,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5580
5632
|
if (buttons.length > 0) {
|
|
5581
5633
|
const choices = document.createElement('template');
|
|
5582
5634
|
choices.setAttribute('slot', 'buttons');
|
|
5583
|
-
for (const
|
|
5635
|
+
for (const choice of buttons) {
|
|
5636
|
+
if (choice instanceof Node) {
|
|
5637
|
+
choices.content.append(choice);
|
|
5638
|
+
continue;
|
|
5639
|
+
}
|
|
5640
|
+
const [result, label, buttonClass] = choice;
|
|
5584
5641
|
const button = document.createElement('button');
|
|
5585
5642
|
button.type = 'button';
|
|
5586
5643
|
button.dataset.result = result;
|
|
5587
|
-
if (
|
|
5588
|
-
button.className =
|
|
5644
|
+
if (buttonClass) {
|
|
5645
|
+
button.className = buttonClass;
|
|
5589
5646
|
}
|
|
5590
5647
|
button.textContent = label;
|
|
5591
5648
|
choices.content.append(button);
|
|
@@ -5607,13 +5664,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5607
5664
|
* @param {string} header
|
|
5608
5665
|
* @param {string|Node} body
|
|
5609
5666
|
* @param {{ confirm?: string, cancel?: string }} [labels] overriding the localized defaults
|
|
5667
|
+
* @param {{ className?: string }} [options] as `ask` takes them
|
|
5610
5668
|
* @returns {Promise<boolean>}
|
|
5611
5669
|
*/
|
|
5612
|
-
static async confirm(header, body, labels = {}) {
|
|
5613
|
-
const outcome = await Dialog.ask(
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5670
|
+
static async confirm(header, body, labels = {}, options = {}) {
|
|
5671
|
+
const outcome = await Dialog.ask(
|
|
5672
|
+
header,
|
|
5673
|
+
body,
|
|
5674
|
+
[
|
|
5675
|
+
['cancel', labels.cancel ?? index_mjs.Localization.of().t('dialog.cancel')],
|
|
5676
|
+
['confirm', labels.confirm ?? index_mjs.Localization.of().t('dialog.confirm'), 'ful-button'],
|
|
5677
|
+
],
|
|
5678
|
+
options,
|
|
5679
|
+
);
|
|
5617
5680
|
return !outcome.dismissed && outcome.result === 'confirm';
|
|
5618
5681
|
}
|
|
5619
5682
|
}
|
|
@@ -5820,6 +5883,65 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5820
5883
|
}
|
|
5821
5884
|
}
|
|
5822
5885
|
|
|
5886
|
+
/**
|
|
5887
|
+
* A menu of commands on the native popover platform: the invoker named by
|
|
5888
|
+
* `for` toggles it and light dismiss and Escape come from the browser, while
|
|
5889
|
+
* the element adds the menu semantics and the keyboard protocol.
|
|
5890
|
+
*
|
|
5891
|
+
* The items are the buttons and links written inside it, kept as they are and
|
|
5892
|
+
* given their menu role and their place in the roving focus. Activation is the
|
|
5893
|
+
* item's own click, which the element only follows by closing.
|
|
5894
|
+
*/
|
|
5895
|
+
class Menu extends index_mjs.ParsedElement {
|
|
5896
|
+
static attributes = ['for'];
|
|
5897
|
+
render() {
|
|
5898
|
+
const named = this.declared('for');
|
|
5899
|
+
const invoker = named ? document.getElementById(named) : null;
|
|
5900
|
+
if (!invoker) {
|
|
5901
|
+
throw new Error(
|
|
5902
|
+
`ful-menu names no invoker: for="${named ?? ''}" must name an element already in the page when the menu upgrades`,
|
|
5903
|
+
);
|
|
5904
|
+
}
|
|
5905
|
+
this.setAttribute('role', 'menu');
|
|
5906
|
+
if (!this.hasAttribute('popover')) {
|
|
5907
|
+
this.setAttribute('popover', '');
|
|
5908
|
+
}
|
|
5909
|
+
invoker.setAttribute('aria-haspopup', 'menu');
|
|
5910
|
+
Anchors.wire(invoker, this, { prefix: 'ful-menu', invoke: true, expanded: true });
|
|
5911
|
+
wireMenuKeys(invoker, this, { items: () => this.items });
|
|
5912
|
+
this.#mark();
|
|
5913
|
+
this.addEventListener('click', (evt) => {
|
|
5914
|
+
const target = /** @type HTMLElement */ (evt.target);
|
|
5915
|
+
if (this.items.some((item) => item === target || item.contains(target))) {
|
|
5916
|
+
this.hidePopover();
|
|
5917
|
+
}
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5920
|
+
/**
|
|
5921
|
+
* The commands, in the order the arrows walk them: the menu's own buttons
|
|
5922
|
+
* and links, minus the disabled and hidden ones, which keep their role and
|
|
5923
|
+
* leave the walk.
|
|
5924
|
+
*/
|
|
5925
|
+
get items() {
|
|
5926
|
+
return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
|
|
5927
|
+
}
|
|
5928
|
+
/**
|
|
5929
|
+
* Gives every button and link of the menu its item role and takes it out of
|
|
5930
|
+
* the tab order, and answers them. Read on every gesture as well as at the
|
|
5931
|
+
* render, so a menu a page fills or empties later needs no rewiring.
|
|
5932
|
+
*/
|
|
5933
|
+
#mark() {
|
|
5934
|
+
const all = /** @type HTMLElement[] */ ([...this.querySelectorAll(':scope > button, :scope > a[href]')]);
|
|
5935
|
+
for (const item of all) {
|
|
5936
|
+
if (!item.hasAttribute('role')) {
|
|
5937
|
+
item.setAttribute('role', 'menuitem');
|
|
5938
|
+
}
|
|
5939
|
+
item.tabIndex = -1;
|
|
5940
|
+
}
|
|
5941
|
+
return all;
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5823
5945
|
const SEVERITIES = ['info', 'success', 'warning', 'error'];
|
|
5824
5946
|
|
|
5825
5947
|
//the regions alive in the document: the show-toast listener is wired once and
|
|
@@ -6465,6 +6587,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6465
6587
|
.defineElement('ful-tooltip', Tooltip)
|
|
6466
6588
|
.defineElement('ful-dialog', Dialog)
|
|
6467
6589
|
.defineElement('ful-drawer', Drawer)
|
|
6590
|
+
.defineElement('ful-menu', Menu)
|
|
6468
6591
|
.defineElement('ful-toasts', Toasts)
|
|
6469
6592
|
.defineElement('ful-tabs', Tabs)
|
|
6470
6593
|
.defineElement('ful-accordion', Accordion)
|
|
@@ -6529,6 +6652,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6529
6652
|
exports.LocalDate = LocalDate;
|
|
6530
6653
|
exports.LocalDateFilter = LocalDateFilter;
|
|
6531
6654
|
exports.LocalStorage = LocalStorage;
|
|
6655
|
+
exports.Menu = Menu;
|
|
6532
6656
|
exports.NumberFilter = NumberFilter;
|
|
6533
6657
|
exports.Pagination = Pagination;
|
|
6534
6658
|
exports.Plugin = Plugin;
|