@getaster/elements 0.0.0-stage → 0.1.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +6 -2
- package/bin/aster-theme-check.js +317 -0
- package/dist/base/drag.d.ts +45 -0
- package/dist/base/drag.js +139 -0
- package/dist/base/element.d.ts +51 -0
- package/dist/base/element.js +133 -0
- package/dist/breadcrumbs/breadcrumbs.d.ts +23 -0
- package/dist/breadcrumbs/breadcrumbs.js +51 -0
- package/dist/button/button.d.ts +37 -0
- package/dist/button/button.js +108 -0
- package/dist/caret/caret-group.d.ts +14 -0
- package/dist/caret/caret-group.js +71 -0
- package/dist/carousel/carousel.d.ts +28 -0
- package/dist/carousel/carousel.js +134 -0
- package/dist/component/component.d.ts +73 -0
- package/dist/component/component.js +190 -0
- package/dist/component/echarts.d.ts +46 -0
- package/dist/component/echarts.js +95 -0
- package/dist/composition/authoring.d.ts +157 -0
- package/dist/composition/authoring.js +291 -0
- package/dist/composition/builtins.d.ts +1130 -0
- package/dist/composition/builtins.js +318 -0
- package/dist/composition/command-bar.d.ts +18 -0
- package/dist/composition/command-bar.js +34 -0
- package/dist/composition/commands.d.ts +14 -0
- package/dist/composition/commands.js +59 -0
- package/dist/composition/composite.d.ts +48 -0
- package/dist/composition/composite.js +409 -0
- package/dist/composition/contracts.d.ts +63 -0
- package/dist/composition/contracts.js +97 -0
- package/dist/composition/definition.d.ts +94 -0
- package/dist/composition/definition.js +291 -0
- package/dist/composition/element.d.ts +35 -0
- package/dist/composition/element.js +51 -0
- package/dist/composition/registry.d.ts +83 -0
- package/dist/composition/registry.js +99 -0
- package/dist/composition/schema.d.ts +9 -0
- package/dist/composition/schema.js +139 -0
- package/dist/composition/sessions.d.ts +15 -0
- package/dist/composition/sessions.js +61 -0
- package/dist/dashboard/dashboard.d.ts +26 -0
- package/dist/dashboard/dashboard.js +296 -0
- package/dist/dashboard/layout.d.ts +19 -0
- package/dist/dashboard/layout.js +48 -0
- package/dist/dashboard/widget.d.ts +42 -0
- package/dist/dashboard/widget.js +165 -0
- package/dist/date/calendar.d.ts +34 -0
- package/dist/date/calendar.js +137 -0
- package/dist/date/date-field.d.ts +18 -0
- package/dist/date/date-field.js +81 -0
- package/dist/date/dates.d.ts +22 -0
- package/dist/date/dates.js +80 -0
- package/dist/dialog/dialog.d.ts +23 -0
- package/dist/dialog/dialog.js +70 -0
- package/dist/field/bound-field.d.ts +55 -0
- package/dist/field/bound-field.js +139 -0
- package/dist/field/check.d.ts +11 -0
- package/dist/field/check.js +26 -0
- package/dist/field/composite-field.d.ts +35 -0
- package/dist/field/composite-field.js +88 -0
- package/dist/field/field.d.ts +29 -0
- package/dist/field/field.js +75 -0
- package/dist/field/input.d.ts +15 -0
- package/dist/field/input.js +18 -0
- package/dist/field/number-field.d.ts +33 -0
- package/dist/field/number-field.js +179 -0
- package/dist/field/numeric.d.ts +4 -0
- package/dist/field/numeric.js +57 -0
- package/dist/field/values.d.ts +14 -0
- package/dist/field/values.js +60 -0
- package/dist/filter/filter-builder.d.ts +18 -0
- package/dist/filter/filter-builder.js +305 -0
- package/dist/form/controls.d.ts +20 -0
- package/dist/form/controls.js +34 -0
- package/dist/form/form.d.ts +46 -0
- package/dist/form/form.js +85 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.js +57 -0
- package/dist/grid/filters.d.ts +16 -0
- package/dist/grid/filters.js +66 -0
- package/dist/grid/grid.d.ts +209 -0
- package/dist/grid/grid.js +2587 -0
- package/dist/grid/rows.d.ts +32 -0
- package/dist/grid/rows.js +89 -0
- package/dist/grid/view.d.ts +69 -0
- package/dist/grid/view.js +172 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +61 -0
- package/dist/layout/stack.d.ts +112 -0
- package/dist/layout/stack.js +21 -0
- package/dist/list-edit/list-edit.d.ts +42 -0
- package/dist/list-edit/list-edit.js +220 -0
- package/dist/listbox/listbox.d.ts +45 -0
- package/dist/listbox/listbox.js +187 -0
- package/dist/menu/menu.d.ts +55 -0
- package/dist/menu/menu.js +286 -0
- package/dist/motion.d.ts +8 -0
- package/dist/motion.js +23 -0
- package/dist/overlay/anchor.d.ts +18 -0
- package/dist/overlay/anchor.js +41 -0
- package/dist/picker/options.d.ts +59 -0
- package/dist/picker/options.js +97 -0
- package/dist/picker/picker.d.ts +46 -0
- package/dist/picker/picker.js +492 -0
- package/dist/picker/tree-choices.d.ts +38 -0
- package/dist/picker/tree-choices.js +72 -0
- package/dist/picker/tree-picker.d.ts +29 -0
- package/dist/picker/tree-picker.js +282 -0
- package/dist/progress/policy.d.ts +5 -0
- package/dist/progress/policy.js +28 -0
- package/dist/progress/status.d.ts +20 -0
- package/dist/progress/status.js +38 -0
- package/dist/rule/operators.d.ts +8 -0
- package/dist/rule/operators.js +9 -0
- package/dist/rule/rule-editor.d.ts +57 -0
- package/dist/rule/rule-editor.js +290 -0
- package/dist/section/accordion.d.ts +24 -0
- package/dist/section/accordion.js +90 -0
- package/dist/section/section.d.ts +30 -0
- package/dist/section/section.js +99 -0
- package/dist/select/choice-element.d.ts +20 -0
- package/dist/select/choice-element.js +55 -0
- package/dist/select/choice.d.ts +34 -0
- package/dist/select/choice.js +122 -0
- package/dist/select/many.d.ts +13 -0
- package/dist/select/many.js +20 -0
- package/dist/select/select.d.ts +16 -0
- package/dist/select/select.js +40 -0
- package/dist/sidebar/sidebar.d.ts +47 -0
- package/dist/sidebar/sidebar.js +105 -0
- package/dist/slicer/slicer.d.ts +26 -0
- package/dist/slicer/slicer.js +140 -0
- package/dist/split/split.d.ts +41 -0
- package/dist/split/split.js +237 -0
- package/dist/styles/contract.d.ts +36 -0
- package/dist/styles/contract.js +39 -0
- package/dist/switch/switch.d.ts +22 -0
- package/dist/switch/switch.js +69 -0
- package/dist/tabs/tabs.d.ts +30 -0
- package/dist/tabs/tabs.js +202 -0
- package/dist/tile-grid/scroll.d.ts +9 -0
- package/dist/tile-grid/scroll.js +21 -0
- package/dist/tile-grid/tile-grid.d.ts +64 -0
- package/dist/tile-grid/tile-grid.js +734 -0
- package/dist/toast/toast.d.ts +25 -0
- package/dist/toast/toast.js +92 -0
- package/dist/tree/navigator.d.ts +41 -0
- package/dist/tree/navigator.js +228 -0
- package/dist/window/chrome.d.ts +3 -0
- package/dist/window/chrome.js +7 -0
- package/dist/window/window.d.ts +46 -0
- package/dist/window/window.js +347 -0
- package/dist/window/workspace.d.ts +29 -0
- package/dist/window/workspace.js +137 -0
- package/package.json +45 -4
- package/styles/aster-ui.css +1147 -0
- package/styles/base.css +88 -0
- package/styles/contract.ts +41 -0
- package/styles/elements.css +1058 -0
- package/styles/harbour.css +126 -0
- package/styles/lamplight.css +118 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { signal } from "@preact/signals-core";
|
|
2
|
+
import { html, nothing } from "lit-html";
|
|
3
|
+
import { AsterElement, define, provideScope, uid } from "../base/element.js";
|
|
4
|
+
import { windowBar } from "../window/chrome.js";
|
|
5
|
+
import { openWhen } from "../overlay/anchor.js";
|
|
6
|
+
import { Menu } from "../menu/menu.js";
|
|
7
|
+
/** The dashboard owns sessions; native DOM moves only reconnect the view. */
|
|
8
|
+
export class DashboardPortlet extends AsterElement {
|
|
9
|
+
#widget = this.prop(undefined);
|
|
10
|
+
#maximized = this.prop(false);
|
|
11
|
+
#busy = this.prop(false);
|
|
12
|
+
#status = this.prop("unavailable");
|
|
13
|
+
#config = signal({});
|
|
14
|
+
#id = uid("aster-widget");
|
|
15
|
+
#scope;
|
|
16
|
+
#actions;
|
|
17
|
+
#session;
|
|
18
|
+
#abort;
|
|
19
|
+
#generation = 0;
|
|
20
|
+
#started = false;
|
|
21
|
+
initialize(widget, scope, actions) {
|
|
22
|
+
this.#scope = scope;
|
|
23
|
+
this.#actions = actions;
|
|
24
|
+
provideScope(this, scope);
|
|
25
|
+
this.setAttribute("role", "listitem");
|
|
26
|
+
this.update(widget, false, false);
|
|
27
|
+
}
|
|
28
|
+
update(widget, maximized, busy) {
|
|
29
|
+
this.#widget.value = widget;
|
|
30
|
+
this.#maximized.value = maximized;
|
|
31
|
+
this.#busy.value = busy;
|
|
32
|
+
if (JSON.stringify(widget.config ?? {}) !== JSON.stringify(this.#config.peek()))
|
|
33
|
+
this.#config.value = widget.config ?? {};
|
|
34
|
+
this.dataset.widget = widget.id;
|
|
35
|
+
this.dataset.width = String(widget.width);
|
|
36
|
+
this.dataset.height = String(widget.height);
|
|
37
|
+
}
|
|
38
|
+
async mount(type) {
|
|
39
|
+
if (!type || this.#started)
|
|
40
|
+
return;
|
|
41
|
+
this.#started = true;
|
|
42
|
+
const generation = ++this.#generation;
|
|
43
|
+
const abort = new AbortController();
|
|
44
|
+
this.#abort = abort;
|
|
45
|
+
this.#status.value = "loading";
|
|
46
|
+
try {
|
|
47
|
+
const session = await type.create({ id: this.#widget.peek().id, config: this.#config, scope: this.#scope, signal: abort.signal });
|
|
48
|
+
if (generation !== this.#generation || abort.signal.aborted) {
|
|
49
|
+
session.dispose();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.#session = session;
|
|
53
|
+
this.#status.value = "ready";
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (generation === this.#generation)
|
|
57
|
+
this.#fail(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async allowRemove() { return await this.#session?.beforeRemove?.() ?? true; }
|
|
61
|
+
dispose() {
|
|
62
|
+
this.#generation++;
|
|
63
|
+
this.#abort?.abort();
|
|
64
|
+
this.#abort = undefined;
|
|
65
|
+
const session = this.#session;
|
|
66
|
+
this.#session = undefined;
|
|
67
|
+
this.#started = false;
|
|
68
|
+
try {
|
|
69
|
+
session?.dispose();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
this.#report(error);
|
|
73
|
+
}
|
|
74
|
+
this.#status.value = "unavailable";
|
|
75
|
+
}
|
|
76
|
+
focusActions() { this.querySelector(".aster-widget-actions")?.focus(); }
|
|
77
|
+
render() {
|
|
78
|
+
const widget = this.#widget.value;
|
|
79
|
+
if (!widget)
|
|
80
|
+
return nothing;
|
|
81
|
+
const max = this.#maximized.value;
|
|
82
|
+
const busy = this.#busy.value;
|
|
83
|
+
return html `<section class="aster-portlet" data-maximized=${String(max)} role=${max ? "dialog" : "region"}
|
|
84
|
+
aria-modal=${max ? "false" : nothing} aria-labelledby=${this.#id} popover=${max ? "manual" : nothing}
|
|
85
|
+
${openWhen(max)} @keydown=${(event) => {
|
|
86
|
+
if (max && event.key === "Escape" && !event.defaultPrevented) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
this.#actions.maximize();
|
|
90
|
+
}
|
|
91
|
+
}}>
|
|
92
|
+
${windowBar(this.#id, widget.title, html `
|
|
93
|
+
<button type="button" aria-label=${`Move ${widget.title}`} ?disabled=${busy || max}
|
|
94
|
+
aria-describedby=${`${this.#id}-move`} @pointerdown=${(event) => this.#actions.drag(event)}
|
|
95
|
+
@keydown=${(event) => this.#key(event, false)}>
|
|
96
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2v12M2 8h12M8 2 6 4m2-2 2 2M2 8l2-2m-2 2 2 2M14 8l-2-2m2 2-2 2M8 14l-2-2m2 2 2-2"/></svg>
|
|
97
|
+
</button>
|
|
98
|
+
<span id=${`${this.#id}-move`} hidden>Drag onto another widget, or use arrow keys to move earlier or later. The actions menu offers the same controls.</span>
|
|
99
|
+
<button type="button" aria-label=${`Resize ${widget.title}`} ?disabled=${busy || max}
|
|
100
|
+
aria-describedby=${`${this.#id}-resize`} @keydown=${(event) => this.#key(event, true)}>
|
|
101
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 13 13 3M7 3h6v6M3 7v6h6"/></svg>
|
|
102
|
+
</button>
|
|
103
|
+
<span id=${`${this.#id}-resize`} hidden>Left and right change width; up and down change height. The actions menu offers the same controls.</span>
|
|
104
|
+
<button type="button" class="aster-widget-actions" aria-label=${`Actions for ${widget.title}`} aria-haspopup="menu" ?disabled=${busy}
|
|
105
|
+
@click=${(event) => this.#menu(event.currentTarget)}>
|
|
106
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h.1M8 8h.1M13 8h.1" stroke-width="3"/></svg>
|
|
107
|
+
</button>
|
|
108
|
+
<button type="button" class="aster-widget-maximize" aria-label=${`${max ? "Restore" : "Maximize"} ${widget.title}`} ?disabled=${busy}
|
|
109
|
+
@click=${() => this.#actions.maximize()}><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3h10v10H3zM3 6h10"/></svg></button>
|
|
110
|
+
<button type="button" aria-label=${`Remove ${widget.title}`} ?disabled=${busy} @click=${() => this.#actions.remove()}>
|
|
111
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m4 4 8 8m0-8-8 8"/></svg></button>`)}
|
|
112
|
+
<div class="aster-portlet-body" aria-busy=${String(this.#status.value === "loading")}>${this.#body()}</div>
|
|
113
|
+
<aster-menu></aster-menu>
|
|
114
|
+
</section>`;
|
|
115
|
+
}
|
|
116
|
+
#body() {
|
|
117
|
+
switch (this.#status.value) {
|
|
118
|
+
case "ready":
|
|
119
|
+
try {
|
|
120
|
+
return this.#session.render();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const generation = this.#generation;
|
|
124
|
+
queueMicrotask(() => { if (generation === this.#generation && this.#status.peek() !== "error")
|
|
125
|
+
this.#fail(error); });
|
|
126
|
+
return html `<p role="status">Widget content unavailable.</p>`;
|
|
127
|
+
}
|
|
128
|
+
case "loading": return html `<p role="status">Loading widget…</p>`;
|
|
129
|
+
case "error": return html `<p role="status">Widget content unavailable. <button type="button" class="aster-button" @click=${() => this.#actions.retry()}>Retry widget</button></p>`;
|
|
130
|
+
default: return html `<p role="status">This widget type is unavailable. Its saved configuration is retained.</p>`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
#fail(error) { this.#status.value = "error"; this.#report(error); }
|
|
134
|
+
#report(error) { this.emit("widgeterror", { id: this.#widget.peek().id, error }); }
|
|
135
|
+
#key(event, resize) {
|
|
136
|
+
if (this.#busy.peek() || this.#maximized.peek())
|
|
137
|
+
return;
|
|
138
|
+
const changes = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
|
|
139
|
+
const by = changes[event.key];
|
|
140
|
+
if (!by)
|
|
141
|
+
return;
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
if (resize)
|
|
144
|
+
this.#actions.resize(by[0], by[1]);
|
|
145
|
+
else
|
|
146
|
+
this.#actions.move(by[0] || by[1]);
|
|
147
|
+
}
|
|
148
|
+
#menu(anchor) {
|
|
149
|
+
const widget = this.#widget.peek();
|
|
150
|
+
const edges = this.#actions.edges();
|
|
151
|
+
const disabled = this.#maximized.peek() ? "Restore this widget before changing its layout." : undefined;
|
|
152
|
+
const action = (label, reason, run) => ({ label, ...(reason ? { disabled: reason } : {}), run });
|
|
153
|
+
const items = [
|
|
154
|
+
action("Move earlier", disabled ?? (edges.first ? "Already first." : undefined), () => this.#actions.move(-1)),
|
|
155
|
+
action("Move later", disabled ?? (edges.last ? "Already last." : undefined), () => this.#actions.move(1)),
|
|
156
|
+
{ kind: "separator" },
|
|
157
|
+
action("Narrower", disabled ?? (widget.width === 1 ? "Already narrowest." : undefined), () => this.#actions.resize(-1, 0)),
|
|
158
|
+
action("Wider", disabled ?? (widget.width === 3 ? "Already widest." : undefined), () => this.#actions.resize(1, 0)),
|
|
159
|
+
action("Shorter", disabled ?? (widget.height === 1 ? "Already shortest." : undefined), () => this.#actions.resize(0, -1)),
|
|
160
|
+
action("Taller", disabled ?? (widget.height === 3 ? "Already tallest." : undefined), () => this.#actions.resize(0, 1)),
|
|
161
|
+
];
|
|
162
|
+
this.querySelector("aster-menu").openFrom(anchor, items, `Layout for ${widget.title}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
define("aster-dashboard-widget", DashboardPortlet);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { nothing, type TemplateResult } from "lit-html";
|
|
2
|
+
/** Whether the calendar is open, and the day that has focus in it. */
|
|
3
|
+
export declare class CalendarState {
|
|
4
|
+
readonly open: import("@preact/signals-core").Signal<boolean>;
|
|
5
|
+
readonly focused: import("@preact/signals-core").Signal<string>;
|
|
6
|
+
/** Set on opening: the focused day takes focus once rendered. */
|
|
7
|
+
focusPending: boolean;
|
|
8
|
+
show(value: string | undefined, today: string): void;
|
|
9
|
+
}
|
|
10
|
+
/** Closes the calendar on a press outside `root`, until it closes. */
|
|
11
|
+
export declare function closeOutside(state: CalendarState, root: Element): void;
|
|
12
|
+
export interface CalendarProps {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** The anchor's `anchor-name`, without the dashes. */
|
|
15
|
+
readonly anchor: string;
|
|
16
|
+
readonly value: string | undefined;
|
|
17
|
+
readonly today: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly choose: (date: string) => void;
|
|
20
|
+
/** Closed without a choice: Escape, or a press outside. */
|
|
21
|
+
readonly dismiss: (refocus: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The APG date-picker dialog: a month grid in a top-layer popover.
|
|
25
|
+
*
|
|
26
|
+
* Keyboard in the grid: arrows by day and week, Page Up/Down by month (with
|
|
27
|
+
* Shift, by year), Home/End to the week's start and end, Enter or Space
|
|
28
|
+
* choose. Tab cycles through the dialog's buttons and the grid; Escape
|
|
29
|
+
* closes. The focused day is the grid's one tab stop, and moving it to
|
|
30
|
+
* another month turns the page.
|
|
31
|
+
*/
|
|
32
|
+
export declare function calendarView(state: CalendarState, p: CalendarProps): TemplateResult | typeof nothing;
|
|
33
|
+
/** Where a key moves the focused day, or undefined if it doesn't. */
|
|
34
|
+
export declare function gridKey(e: Pick<KeyboardEvent, "key" | "shiftKey">, from: string): string | undefined;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { signal } from "@preact/signals-core";
|
|
2
|
+
import { html, nothing } from "lit-html";
|
|
3
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
4
|
+
import { openWhen, placeStyle } from "../overlay/anchor.js";
|
|
5
|
+
import { monthWeeks, shift, weekBounds } from "./dates.js";
|
|
6
|
+
const MONTH = new Intl.DateTimeFormat("en-GB", { month: "long", year: "numeric", timeZone: "UTC" });
|
|
7
|
+
const DAY = new Intl.DateTimeFormat("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
|
|
8
|
+
const WEEKDAYS = [
|
|
9
|
+
["Mo", "Monday"],
|
|
10
|
+
["Tu", "Tuesday"],
|
|
11
|
+
["We", "Wednesday"],
|
|
12
|
+
["Th", "Thursday"],
|
|
13
|
+
["Fr", "Friday"],
|
|
14
|
+
["Sa", "Saturday"],
|
|
15
|
+
["Su", "Sunday"],
|
|
16
|
+
];
|
|
17
|
+
/** Whether the calendar is open, and the day that has focus in it. */
|
|
18
|
+
export class CalendarState {
|
|
19
|
+
open = signal(false);
|
|
20
|
+
focused = signal("");
|
|
21
|
+
/** Set on opening: the focused day takes focus once rendered. */
|
|
22
|
+
focusPending = false;
|
|
23
|
+
show(value, today) {
|
|
24
|
+
this.focused.value = value && /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : today;
|
|
25
|
+
this.focusPending = true;
|
|
26
|
+
this.open.value = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Closes the calendar on a press outside `root`, until it closes. */
|
|
30
|
+
export function closeOutside(state, root) {
|
|
31
|
+
const outside = (e) => {
|
|
32
|
+
if (!root.contains(e.target))
|
|
33
|
+
state.open.value = false;
|
|
34
|
+
};
|
|
35
|
+
document.addEventListener("pointerdown", outside, { capture: true });
|
|
36
|
+
const stop = state.open.subscribe((open) => {
|
|
37
|
+
if (open)
|
|
38
|
+
return;
|
|
39
|
+
document.removeEventListener("pointerdown", outside, { capture: true });
|
|
40
|
+
queueMicrotask(() => stop());
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The APG date-picker dialog: a month grid in a top-layer popover.
|
|
45
|
+
*
|
|
46
|
+
* Keyboard in the grid: arrows by day and week, Page Up/Down by month (with
|
|
47
|
+
* Shift, by year), Home/End to the week's start and end, Enter or Space
|
|
48
|
+
* choose. Tab cycles through the dialog's buttons and the grid; Escape
|
|
49
|
+
* closes. The focused day is the grid's one tab stop, and moving it to
|
|
50
|
+
* another month turns the page.
|
|
51
|
+
*/
|
|
52
|
+
export function calendarView(state, p) {
|
|
53
|
+
if (!state.open.value)
|
|
54
|
+
return nothing;
|
|
55
|
+
const focused = state.focused.value;
|
|
56
|
+
const headingId = `${p.id}-month`;
|
|
57
|
+
const move = (to) => {
|
|
58
|
+
state.focused.value = to;
|
|
59
|
+
queueMicrotask(() => document.getElementById(`${p.id}-${to}`)?.focus());
|
|
60
|
+
};
|
|
61
|
+
const page = (by) => (state.focused.value = shift(focused, by));
|
|
62
|
+
const grid = (e) => {
|
|
63
|
+
const to = gridKey(e, focused);
|
|
64
|
+
if (to) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
move(to);
|
|
67
|
+
}
|
|
68
|
+
else if (e.key === "Enter" || e.key === " ") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
p.choose(focused);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const dialog = (e) => {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
if (e.key === "Escape") {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
p.dismiss(true);
|
|
78
|
+
}
|
|
79
|
+
else if (e.key === "Tab") {
|
|
80
|
+
const stops = [...e.currentTarget.querySelectorAll("button, [tabindex='0']")];
|
|
81
|
+
const at = stops.indexOf(document.activeElement);
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
stops[(at + (e.shiftKey ? -1 : 1) + stops.length) % stops.length]?.focus();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const month = focused.slice(0, 7);
|
|
87
|
+
return html `<div class="aster-calendar aster-popover" popover="manual" role="dialog" aria-modal="true" aria-label=${p.label}
|
|
88
|
+
id=${p.id} style=${placeStyle(p.anchor)} ${openWhen(true)} @keydown=${dialog}
|
|
89
|
+
${ref(() => {
|
|
90
|
+
if (!state.focusPending)
|
|
91
|
+
return;
|
|
92
|
+
state.focusPending = false;
|
|
93
|
+
requestAnimationFrame(() => document.getElementById(`${p.id}-${state.focused.peek()}`)?.focus());
|
|
94
|
+
})}>
|
|
95
|
+
<div class="aster-calendar-head">
|
|
96
|
+
<button type="button" aria-label="Previous year" @click=${() => page({ years: -1 })}>«</button>
|
|
97
|
+
<button type="button" aria-label="Previous month" @click=${() => page({ months: -1 })}>‹</button>
|
|
98
|
+
<h2 id=${headingId} aria-live="polite">${MONTH.format(Date.parse(`${month}-15T12:00:00Z`))}</h2>
|
|
99
|
+
<button type="button" aria-label="Next month" @click=${() => page({ months: 1 })}>›</button>
|
|
100
|
+
<button type="button" aria-label="Next year" @click=${() => page({ years: 1 })}>»</button>
|
|
101
|
+
</div>
|
|
102
|
+
<table role="grid" aria-labelledby=${headingId} @keydown=${grid}>
|
|
103
|
+
<thead><tr>${WEEKDAYS.map(([short, long]) => html `<th scope="col" abbr=${long}>${short}</th>`)}</tr></thead>
|
|
104
|
+
<tbody>
|
|
105
|
+
${monthWeeks(focused).map((week) => html `<tr>${week.map((day) => day.slice(0, 7) !== month
|
|
106
|
+
? html `<td></td>`
|
|
107
|
+
: html `<td role="gridcell" id=${`${p.id}-${day}`} tabindex=${day === focused ? "0" : "-1"}
|
|
108
|
+
aria-selected=${day === p.value ? "true" : nothing} aria-current=${day === p.today ? "date" : nothing}
|
|
109
|
+
aria-label=${DAY.format(Date.parse(`${day}T12:00:00Z`))}
|
|
110
|
+
@click=${() => p.choose(day)}>${Number(day.slice(8))}</td>`)}</tr>`)}
|
|
111
|
+
</tbody>
|
|
112
|
+
</table>
|
|
113
|
+
</div>`;
|
|
114
|
+
}
|
|
115
|
+
/** Where a key moves the focused day, or undefined if it doesn't. */
|
|
116
|
+
export function gridKey(e, from) {
|
|
117
|
+
switch (e.key) {
|
|
118
|
+
case "ArrowLeft":
|
|
119
|
+
return shift(from, { days: -1 });
|
|
120
|
+
case "ArrowRight":
|
|
121
|
+
return shift(from, { days: 1 });
|
|
122
|
+
case "ArrowUp":
|
|
123
|
+
return shift(from, { days: -7 });
|
|
124
|
+
case "ArrowDown":
|
|
125
|
+
return shift(from, { days: 7 });
|
|
126
|
+
case "PageUp":
|
|
127
|
+
return shift(from, e.shiftKey ? { years: -1 } : { months: -1 });
|
|
128
|
+
case "PageDown":
|
|
129
|
+
return shift(from, e.shiftKey ? { years: 1 } : { months: 1 });
|
|
130
|
+
case "Home":
|
|
131
|
+
return weekBounds(from)[0];
|
|
132
|
+
case "End":
|
|
133
|
+
return weekBounds(from)[1];
|
|
134
|
+
default:
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type View } from "../base/element.ts";
|
|
2
|
+
import { BoundField, type ControlContext } from "../field/bound-field.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A date field: typed dates in the forms people write them ("7 Oct",
|
|
5
|
+
* "7/10/2026", "2026-10-07"), shown as "7 Oct 2026", with a calendar
|
|
6
|
+
* button that opens the APG date-picker dialog. What's typed is kept as
|
|
7
|
+
* typed while the input has focus; text that isn't a date stays text, and
|
|
8
|
+
* validation says so.
|
|
9
|
+
*
|
|
10
|
+
* The calendar button takes focus on press in every engine (macOS engines
|
|
11
|
+
* don't focus buttons on click), so focus never falls to the body between
|
|
12
|
+
* the input and the calendar: a grid row being edited would end its edit.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DateField extends BoundField {
|
|
15
|
+
#private;
|
|
16
|
+
focus(options?: FocusOptions): void;
|
|
17
|
+
protected control(c: ControlContext): View;
|
|
18
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { html, nothing } from "lit-html";
|
|
2
|
+
import { live } from "lit-html/directives/live.js";
|
|
3
|
+
import { define } from "../base/element.js";
|
|
4
|
+
import { BoundField } from "../field/bound-field.js";
|
|
5
|
+
import { anchorStyle } from "../overlay/anchor.js";
|
|
6
|
+
import { CalendarState, calendarView, closeOutside } from "./calendar.js";
|
|
7
|
+
import { formatDate, parseDate, today } from "./dates.js";
|
|
8
|
+
/**
|
|
9
|
+
* A date field: typed dates in the forms people write them ("7 Oct",
|
|
10
|
+
* "7/10/2026", "2026-10-07"), shown as "7 Oct 2026", with a calendar
|
|
11
|
+
* button that opens the APG date-picker dialog. What's typed is kept as
|
|
12
|
+
* typed while the input has focus; text that isn't a date stays text, and
|
|
13
|
+
* validation says so.
|
|
14
|
+
*
|
|
15
|
+
* The calendar button takes focus on press in every engine (macOS engines
|
|
16
|
+
* don't focus buttons on click), so focus never falls to the body between
|
|
17
|
+
* the input and the calendar: a grid row being edited would end its edit.
|
|
18
|
+
*/
|
|
19
|
+
export class DateField extends BoundField {
|
|
20
|
+
#calendar = new CalendarState();
|
|
21
|
+
/** What's being typed, while the input has focus. */
|
|
22
|
+
#draft = this.prop(undefined);
|
|
23
|
+
focus(options) {
|
|
24
|
+
this.querySelector("input")?.focus(options);
|
|
25
|
+
}
|
|
26
|
+
control(c) {
|
|
27
|
+
const value = typeof c.value === "string" ? c.value : undefined;
|
|
28
|
+
const readOnly = c.state.readOnly;
|
|
29
|
+
const open = this.#calendar.open.value;
|
|
30
|
+
const text = this.#draft.value ?? (value === undefined ? "" : formatDate(value));
|
|
31
|
+
const button = `${c.id}-calendar`;
|
|
32
|
+
const close = (refocus) => {
|
|
33
|
+
this.#calendar.open.value = false;
|
|
34
|
+
if (refocus)
|
|
35
|
+
this.querySelector(`#${CSS.escape(button)}`)?.focus();
|
|
36
|
+
};
|
|
37
|
+
return html `<div class="aster-field aster-date" data-ring-owner style=${anchorStyle(c.id)}>
|
|
38
|
+
<input id=${c.id} .value=${live(text)}
|
|
39
|
+
aria-label=${this.labelled ? nothing : c.label}
|
|
40
|
+
placeholder=${c.meta?.nullable ? "None" : "A date, like 7 Oct"}
|
|
41
|
+
?readonly=${readOnly}
|
|
42
|
+
aria-required=${c.state.required ? "true" : nothing}
|
|
43
|
+
aria-invalid=${c.invalid ? "true" : nothing}
|
|
44
|
+
aria-describedby=${c.describedBy ?? nothing}
|
|
45
|
+
autocomplete="off" spellcheck="false"
|
|
46
|
+
@input=${(e) => {
|
|
47
|
+
const t = e.target.value;
|
|
48
|
+
this.#draft.value = t;
|
|
49
|
+
this.write(t.trim() === "" ? null : parseDate(t, Number(today().slice(0, 4))) ?? t);
|
|
50
|
+
}}
|
|
51
|
+
@blur=${() => (this.#draft.value = undefined)}>
|
|
52
|
+
${readOnly
|
|
53
|
+
? nothing
|
|
54
|
+
: html `<button type="button" class="aster-date-button" id=${button} aria-label=${`Choose ${c.label}`}
|
|
55
|
+
aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"} aria-controls=${`${c.id}-dialog`}
|
|
56
|
+
@pointerdown=${(e) => e.currentTarget.focus()}
|
|
57
|
+
@mousedown=${(e) => e.preventDefault()}
|
|
58
|
+
@click=${() => (open ? close(false) : this.#open(value))}>
|
|
59
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2.5" y="3.5" width="11" height="10" rx="1.5"/><path d="M2.5 6.5h11M5.5 2v3M10.5 2v3"/></svg>
|
|
60
|
+
</button>`}
|
|
61
|
+
</div>
|
|
62
|
+
${calendarView(this.#calendar, {
|
|
63
|
+
id: `${c.id}-dialog`,
|
|
64
|
+
anchor: c.id,
|
|
65
|
+
value,
|
|
66
|
+
today: today(),
|
|
67
|
+
label: `Choose ${c.label}`,
|
|
68
|
+
choose: (date) => {
|
|
69
|
+
this.write(date);
|
|
70
|
+
close(false);
|
|
71
|
+
this.querySelector("input")?.focus();
|
|
72
|
+
},
|
|
73
|
+
dismiss: close,
|
|
74
|
+
})}`;
|
|
75
|
+
}
|
|
76
|
+
#open(value) {
|
|
77
|
+
this.#calendar.show(value, today());
|
|
78
|
+
closeOutside(this.#calendar, this);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
define("aster-date", DateField);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function iso(year: number, month: number, day: number): string | undefined;
|
|
2
|
+
/** "7 Oct 2026" for `2026-10-07`; other text unchanged. Month names are ours, the same in every engine. */
|
|
3
|
+
export declare function formatDate(value: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* A typed date: `2026-10-07`, `7/10/2026` (day first), `7 Oct 2026`,
|
|
6
|
+
* `Oct 7 2026`, or `7 Oct` (this year). `undefined` when it isn't one.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseDate(text: string, thisYear: number): string | undefined;
|
|
9
|
+
/** Adds days, months (clamped to the month's end) or years. */
|
|
10
|
+
export declare function shift(value: string, by: {
|
|
11
|
+
days?: number;
|
|
12
|
+
months?: number;
|
|
13
|
+
years?: number;
|
|
14
|
+
}): string;
|
|
15
|
+
/** Monday of `value`'s week, and Sunday. */
|
|
16
|
+
export declare function weekBounds(value: string): [string, string];
|
|
17
|
+
/** The weeks of `value`'s month, Monday to Sunday, padded with the days of the months either side (the calendar leaves those blank). */
|
|
18
|
+
export declare function monthWeeks(value: string): string[][];
|
|
19
|
+
/** Sets where "today" comes from: an app with its own clock (frozen in tests) passes it here. */
|
|
20
|
+
export declare function setClock(clock: () => number): void;
|
|
21
|
+
/** Today as `YYYY-MM-DD`, in UTC like the dates themselves. */
|
|
22
|
+
export declare function today(): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Calendar dates as `YYYY-MM-DD`, with no time zone: what Edm.Date holds.
|
|
2
|
+
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
3
|
+
const SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
4
|
+
export function iso(year, month, day) {
|
|
5
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
6
|
+
if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day)
|
|
7
|
+
return undefined;
|
|
8
|
+
return d.toISOString().slice(0, 10);
|
|
9
|
+
}
|
|
10
|
+
/** "7 Oct 2026" for `2026-10-07`; other text unchanged. Month names are ours, the same in every engine. */
|
|
11
|
+
export function formatDate(value) {
|
|
12
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
13
|
+
return m ? `${Number(m[3])} ${SHORT[Number(m[2]) - 1]} ${m[1]}` : value;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A typed date: `2026-10-07`, `7/10/2026` (day first), `7 Oct 2026`,
|
|
17
|
+
* `Oct 7 2026`, or `7 Oct` (this year). `undefined` when it isn't one.
|
|
18
|
+
*/
|
|
19
|
+
export function parseDate(text, thisYear) {
|
|
20
|
+
const t = text.trim().toLowerCase().replace(/,/g, " ").replace(/\s+/g, " ");
|
|
21
|
+
let m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
22
|
+
if (m)
|
|
23
|
+
return iso(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
24
|
+
m = /^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2}|\d{4})$/.exec(t);
|
|
25
|
+
if (m)
|
|
26
|
+
return iso(year(m[3]), Number(m[2]), Number(m[1]));
|
|
27
|
+
m = /^(\d{1,2}) ([a-z]+)\.?(?: (\d{2}|\d{4}))?$/.exec(t);
|
|
28
|
+
if (m)
|
|
29
|
+
return byName(m[2], Number(m[1]), m[3] ? year(m[3]) : thisYear);
|
|
30
|
+
m = /^([a-z]+)\.? (\d{1,2})(?: (\d{2}|\d{4}))?$/.exec(t);
|
|
31
|
+
if (m)
|
|
32
|
+
return byName(m[1], Number(m[2]), m[3] ? year(m[3]) : thisYear);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function year(text) {
|
|
36
|
+
return text.length === 2 ? 2000 + Number(text) : Number(text);
|
|
37
|
+
}
|
|
38
|
+
function byName(name, day, y) {
|
|
39
|
+
const month = MONTHS.indexOf(name.slice(0, 3));
|
|
40
|
+
return month < 0 ? undefined : iso(y, month + 1, day);
|
|
41
|
+
}
|
|
42
|
+
/** Adds days, months (clamped to the month's end) or years. */
|
|
43
|
+
export function shift(value, by) {
|
|
44
|
+
const [y, m, d] = value.split("-").map(Number);
|
|
45
|
+
if (by.days)
|
|
46
|
+
return new Date(Date.UTC(y, m - 1, d + by.days)).toISOString().slice(0, 10);
|
|
47
|
+
const months = (by.months ?? 0) + (by.years ?? 0) * 12;
|
|
48
|
+
const first = new Date(Date.UTC(y, m - 1 + months, 1));
|
|
49
|
+
const last = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate();
|
|
50
|
+
return iso(first.getUTCFullYear(), first.getUTCMonth() + 1, Math.min(d, last));
|
|
51
|
+
}
|
|
52
|
+
/** Monday of `value`'s week, and Sunday. */
|
|
53
|
+
export function weekBounds(value) {
|
|
54
|
+
const day = (new Date(`${value}T12:00:00Z`).getUTCDay() + 6) % 7;
|
|
55
|
+
return [shift(value, { days: -day }), shift(value, { days: 6 - day })];
|
|
56
|
+
}
|
|
57
|
+
/** The weeks of `value`'s month, Monday to Sunday, padded with the days of the months either side (the calendar leaves those blank). */
|
|
58
|
+
export function monthWeeks(value) {
|
|
59
|
+
const first = `${value.slice(0, 7)}-01`;
|
|
60
|
+
let day = weekBounds(first)[0];
|
|
61
|
+
const weeks = [];
|
|
62
|
+
do {
|
|
63
|
+
const week = [];
|
|
64
|
+
for (let i = 0; i < 7; i++) {
|
|
65
|
+
week.push(day);
|
|
66
|
+
day = shift(day, { days: 1 });
|
|
67
|
+
}
|
|
68
|
+
weeks.push(week);
|
|
69
|
+
} while (day.slice(0, 7) === value.slice(0, 7));
|
|
70
|
+
return weeks;
|
|
71
|
+
}
|
|
72
|
+
let now = () => Date.now();
|
|
73
|
+
/** Sets where "today" comes from: an app with its own clock (frozen in tests) passes it here. */
|
|
74
|
+
export function setClock(clock) {
|
|
75
|
+
now = clock;
|
|
76
|
+
}
|
|
77
|
+
/** Today as `YYYY-MM-DD`, in UTC like the dates themselves. */
|
|
78
|
+
export function today() {
|
|
79
|
+
return new Date(now()).toISOString().slice(0, 10);
|
|
80
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsterElement, type View } from "../base/element.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A modal dialog over a native `<dialog>`: the rest of the page is inert,
|
|
4
|
+
* focus goes in on open and returns to the opener on close, and Escape or
|
|
5
|
+
* a click on the backdrop closes it. One dialog morphs between views
|
|
6
|
+
* (`showView`) rather than stacking modals.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Dialog extends AsterElement {
|
|
9
|
+
#private;
|
|
10
|
+
/** The dialog's contents; usually a template reading the current view. */
|
|
11
|
+
get content(): View;
|
|
12
|
+
set content(v: View);
|
|
13
|
+
/** The id of the element that names the dialog. */
|
|
14
|
+
get labelledBy(): string | undefined;
|
|
15
|
+
set labelledBy(v: string | undefined);
|
|
16
|
+
get open(): boolean;
|
|
17
|
+
/** Opens modally. `focus` is the element to focus inside; otherwise the first focusable. */
|
|
18
|
+
show(opener?: HTMLElement | null, focus?: () => HTMLElement | null): void;
|
|
19
|
+
close(): void;
|
|
20
|
+
/** Changes view in place: the panel morphs, then the new content inks in. */
|
|
21
|
+
showView(change: () => void): void;
|
|
22
|
+
protected render(): View;
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { html, nothing } from "lit-html";
|
|
2
|
+
import { AsterElement, define } from "../base/element.js";
|
|
3
|
+
import { morph } from "../motion.js";
|
|
4
|
+
/**
|
|
5
|
+
* A modal dialog over a native `<dialog>`: the rest of the page is inert,
|
|
6
|
+
* focus goes in on open and returns to the opener on close, and Escape or
|
|
7
|
+
* a click on the backdrop closes it. One dialog morphs between views
|
|
8
|
+
* (`showView`) rather than stacking modals.
|
|
9
|
+
*/
|
|
10
|
+
export class Dialog extends AsterElement {
|
|
11
|
+
#content = this.prop(nothing);
|
|
12
|
+
#label = this.prop(undefined);
|
|
13
|
+
#opener = null;
|
|
14
|
+
/** The dialog's contents; usually a template reading the current view. */
|
|
15
|
+
get content() {
|
|
16
|
+
return this.#content.value;
|
|
17
|
+
}
|
|
18
|
+
set content(v) {
|
|
19
|
+
this.#content.value = v;
|
|
20
|
+
}
|
|
21
|
+
/** The id of the element that names the dialog. */
|
|
22
|
+
get labelledBy() {
|
|
23
|
+
return this.#label.value;
|
|
24
|
+
}
|
|
25
|
+
set labelledBy(v) {
|
|
26
|
+
this.#label.value = v;
|
|
27
|
+
}
|
|
28
|
+
get open() {
|
|
29
|
+
return this.#dialog()?.open ?? false;
|
|
30
|
+
}
|
|
31
|
+
/** Opens modally. `focus` is the element to focus inside; otherwise the first focusable. */
|
|
32
|
+
show(opener = document.activeElement, focus) {
|
|
33
|
+
const dialog = this.#dialog();
|
|
34
|
+
if (!dialog || dialog.open)
|
|
35
|
+
return;
|
|
36
|
+
this.#opener = opener;
|
|
37
|
+
dialog.showModal();
|
|
38
|
+
const target = focus?.();
|
|
39
|
+
if (target)
|
|
40
|
+
target.focus();
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
const dialog = this.#dialog();
|
|
44
|
+
if (!dialog?.open)
|
|
45
|
+
return;
|
|
46
|
+
dialog.close();
|
|
47
|
+
}
|
|
48
|
+
/** Changes view in place: the panel morphs, then the new content inks in. */
|
|
49
|
+
showView(change) {
|
|
50
|
+
morph(change);
|
|
51
|
+
}
|
|
52
|
+
render() {
|
|
53
|
+
return html `<dialog class="aster-dialog" aria-labelledby=${this.#label.value ?? nothing}
|
|
54
|
+
@close=${() => this.#closed()}
|
|
55
|
+
@click=${(e) => e.target === e.currentTarget && this.close()}>
|
|
56
|
+
<div class="aster-dialog-panel">${this.#content.value}</div>
|
|
57
|
+
</dialog>`;
|
|
58
|
+
}
|
|
59
|
+
#closed() {
|
|
60
|
+
const opener = this.#opener;
|
|
61
|
+
this.#opener = null;
|
|
62
|
+
if (opener?.isConnected)
|
|
63
|
+
opener.focus();
|
|
64
|
+
this.emit("close");
|
|
65
|
+
}
|
|
66
|
+
#dialog() {
|
|
67
|
+
return this.querySelector("dialog");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
define("aster-dialog", Dialog);
|