drab 7.0.1 → 7.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/announcer/define.d.ts +1 -0
- package/dist/announcer/define.d.ts.map +1 -0
- package/dist/announcer/index.d.ts +1 -0
- package/dist/announcer/index.d.ts.map +1 -0
- package/dist/base/index.d.ts +1 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/contextmenu/define.d.ts +1 -0
- package/dist/contextmenu/define.d.ts.map +1 -0
- package/dist/contextmenu/index.d.ts +2 -1
- package/dist/contextmenu/index.d.ts.map +1 -0
- package/dist/define.d.ts +1 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/dialog/define.d.ts +1 -0
- package/dist/dialog/define.d.ts.map +1 -0
- package/dist/dialog/index.d.ts +2 -1
- package/dist/dialog/index.d.ts.map +1 -0
- package/dist/editor/define.d.ts +1 -0
- package/dist/editor/define.d.ts.map +1 -0
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/fullscreen/define.d.ts +1 -0
- package/dist/fullscreen/define.d.ts.map +1 -0
- package/dist/fullscreen/index.d.ts +2 -1
- package/dist/fullscreen/index.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/intersect/define.d.ts +1 -0
- package/dist/intersect/define.d.ts.map +1 -0
- package/dist/intersect/index.d.ts +2 -1
- package/dist/intersect/index.d.ts.map +1 -0
- package/dist/prefetch/define.d.ts +1 -0
- package/dist/prefetch/define.d.ts.map +1 -0
- package/dist/prefetch/index.d.ts +1 -0
- package/dist/prefetch/index.d.ts.map +1 -0
- package/dist/share/define.d.ts +1 -0
- package/dist/share/define.d.ts.map +1 -0
- package/dist/share/index.d.ts +2 -1
- package/dist/share/index.d.ts.map +1 -0
- package/dist/tablesort/define.d.ts +1 -0
- package/dist/tablesort/define.d.ts.map +1 -0
- package/dist/tablesort/index.d.ts +2 -1
- package/dist/tablesort/index.d.ts.map +1 -0
- package/dist/tabs/define.d.ts +1 -0
- package/dist/tabs/define.d.ts.map +1 -0
- package/dist/tabs/index.d.ts +1 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/util/define.d.ts +1 -0
- package/dist/util/define.d.ts.map +1 -0
- package/dist/util/validate.d.ts +1 -0
- package/dist/util/validate.d.ts.map +1 -0
- package/dist/wakelock/define.d.ts +1 -0
- package/dist/wakelock/define.d.ts.map +1 -0
- package/dist/wakelock/index.d.ts +2 -1
- package/dist/wakelock/index.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/announcer/define.ts +4 -0
- package/src/announcer/index.ts +93 -0
- package/src/base/index.ts +290 -0
- package/src/contextmenu/define.ts +4 -0
- package/src/contextmenu/index.ts +95 -0
- package/src/define.ts +11 -0
- package/src/dialog/define.ts +4 -0
- package/src/dialog/index.ts +120 -0
- package/src/editor/define.ts +4 -0
- package/src/editor/index.ts +448 -0
- package/src/fullscreen/define.ts +4 -0
- package/src/fullscreen/index.ts +59 -0
- package/src/index.ts +11 -0
- package/src/intersect/define.ts +4 -0
- package/src/intersect/index.ts +79 -0
- package/src/prefetch/define.ts +4 -0
- package/src/prefetch/index.ts +195 -0
- package/src/share/define.ts +4 -0
- package/src/share/index.ts +99 -0
- package/src/tablesort/define.ts +4 -0
- package/src/tablesort/index.ts +168 -0
- package/src/tabs/define.ts +4 -0
- package/src/tabs/index.ts +173 -0
- package/src/types/index.ts +39 -0
- package/src/util/define.ts +10 -0
- package/src/util/validate.ts +16 -0
- package/src/wakelock/define.ts +4 -0
- package/src/wakelock/index.ts +133 -0
@@ -0,0 +1,290 @@
|
|
1
|
+
import { Announcer } from "../announcer/index.js";
|
2
|
+
import { validate } from "../util/validate.js";
|
3
|
+
|
4
|
+
export interface TriggerAttributes {
|
5
|
+
trigger?: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface ContentAttributes {
|
9
|
+
content?: string;
|
10
|
+
swap?: string;
|
11
|
+
}
|
12
|
+
|
13
|
+
export type Constructor<T> = new (...args: any[]) => T;
|
14
|
+
|
15
|
+
export const Lifecycle = <T extends Constructor<HTMLElement>>(
|
16
|
+
Super = HTMLElement as T,
|
17
|
+
) =>
|
18
|
+
class Lifecycle extends Super {
|
19
|
+
/** To clean up event listeners added to `document` when the element is removed. */
|
20
|
+
#listenerController = new AbortController();
|
21
|
+
|
22
|
+
constructor(...args: any[]) {
|
23
|
+
super(...args);
|
24
|
+
}
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Wrapper around `addEventListener` that ensures when the element is
|
28
|
+
* removed from the DOM, these event listeners are cleaned up.
|
29
|
+
*
|
30
|
+
* @param type Event listener type - ex: `"keydown"`
|
31
|
+
* @param listener Listener to add to the target.
|
32
|
+
* @param target Event target to add the listener to - defaults to `document.body`.
|
33
|
+
* @param options Other options sans `signal`.
|
34
|
+
*/
|
35
|
+
safeListener<T extends keyof HTMLElementEventMap>(
|
36
|
+
type: T,
|
37
|
+
listener: (this: HTMLElement, event: HTMLElementEventMap[T]) => any,
|
38
|
+
element?: HTMLElement,
|
39
|
+
options?: AddEventListenerOptions,
|
40
|
+
): void;
|
41
|
+
safeListener<T extends keyof DocumentEventMap>(
|
42
|
+
type: T,
|
43
|
+
listener: (this: Document, event: DocumentEventMap[T]) => any,
|
44
|
+
document: Document,
|
45
|
+
options?: AddEventListenerOptions,
|
46
|
+
): void;
|
47
|
+
safeListener<T extends keyof WindowEventMap>(
|
48
|
+
type: T,
|
49
|
+
listener: (this: Window, event: WindowEventMap[T]) => any,
|
50
|
+
window: Window,
|
51
|
+
options?: AddEventListenerOptions,
|
52
|
+
): void;
|
53
|
+
safeListener(
|
54
|
+
type: string,
|
55
|
+
listener: EventListenerOrEventListenerObject,
|
56
|
+
target: EventTarget = document.body,
|
57
|
+
options: AddEventListenerOptions = {},
|
58
|
+
) {
|
59
|
+
options.signal = this.#listenerController.signal;
|
60
|
+
target.addEventListener(type, listener, options);
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Passed into `queueMicrotask` in `connectedCallback`.
|
65
|
+
* It is overridden in each component that needs to run `connectedCallback`.
|
66
|
+
*
|
67
|
+
* The reason for this is to make these elements work better with frameworks like Svelte.
|
68
|
+
* For SSR this isn't necessary, but when client side rendering, the HTML within the
|
69
|
+
* custom element isn't available before `connectedCallback` is called. By waiting until
|
70
|
+
* the next microtask, the HTML content is available---then for example, listeners can
|
71
|
+
* be attached to elements inside.
|
72
|
+
*/
|
73
|
+
mount() {}
|
74
|
+
|
75
|
+
/** Called when custom element is added to the page. */
|
76
|
+
connectedCallback() {
|
77
|
+
queueMicrotask(() => this.mount());
|
78
|
+
}
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Passed into `disconnectedCallback`, since `Base` needs to run `disconnectedCallback` as well. It is overridden in each element that needs to run `disconnectedCallback`.
|
82
|
+
*/
|
83
|
+
destroy() {}
|
84
|
+
|
85
|
+
/** Called when custom element is removed from the page. */
|
86
|
+
disconnectedCallback() {
|
87
|
+
this.destroy();
|
88
|
+
this.#listenerController.abort();
|
89
|
+
}
|
90
|
+
};
|
91
|
+
|
92
|
+
type Listener<T extends keyof HTMLElementEventMap> = (
|
93
|
+
this: HTMLElement,
|
94
|
+
e: HTMLElementEventMap[T],
|
95
|
+
) => any;
|
96
|
+
|
97
|
+
/**
|
98
|
+
* By default, `trigger`s are selected via the `data-trigger` attribute.
|
99
|
+
* Alternatively, you can set the `trigger` attribute to a CSS selector to
|
100
|
+
* change the default selector from `[data-trigger]` to a selector of your
|
101
|
+
* choosing. This can be useful if you have multiple elements within one another.
|
102
|
+
*
|
103
|
+
* Each element can have multiple `trigger`s.
|
104
|
+
*/
|
105
|
+
export const Trigger = <T extends Constructor<HTMLElement>>(
|
106
|
+
Super = HTMLElement as T,
|
107
|
+
) =>
|
108
|
+
class Trigger extends Super {
|
109
|
+
constructor(...args: any[]) {
|
110
|
+
super(...args);
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* Event for the `trigger` to execute.
|
115
|
+
*
|
116
|
+
* For example, set to `"mouseover"` to execute the event when the user hovers the mouse over the `trigger`, instead of when they click it.
|
117
|
+
*
|
118
|
+
* @default "click"
|
119
|
+
*/
|
120
|
+
get event(): keyof HTMLElementEventMap {
|
121
|
+
return (
|
122
|
+
(this.getAttribute("event") as keyof HTMLElementEventMap) ?? "click"
|
123
|
+
);
|
124
|
+
}
|
125
|
+
|
126
|
+
set event(value) {
|
127
|
+
this.setAttribute("event", value);
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* @param instance The instance of the desired element to validate against,
|
132
|
+
* ex: `HTMLButtonElement`. Defaults to `HTMLElement`.
|
133
|
+
* @returns All of the elements that match the `trigger` selector.
|
134
|
+
* @default this.querySelectorAll("[data-trigger]")
|
135
|
+
*/
|
136
|
+
triggers<T extends HTMLElement>(instance: Constructor<T>): NodeListOf<T>;
|
137
|
+
triggers(): NodeListOf<HTMLElement>;
|
138
|
+
triggers(instance = HTMLElement) {
|
139
|
+
const triggers = this.querySelectorAll(
|
140
|
+
this.getAttribute("trigger") ?? "[data-trigger]",
|
141
|
+
);
|
142
|
+
|
143
|
+
for (const trigger of triggers) validate(trigger, instance);
|
144
|
+
|
145
|
+
return triggers;
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* @param listener Listener to attach to all of the `trigger` elements.
|
150
|
+
* @param options
|
151
|
+
*/
|
152
|
+
listener<T extends keyof HTMLElementEventMap>(
|
153
|
+
listener: Listener<T>,
|
154
|
+
options?: AddEventListenerOptions,
|
155
|
+
): void;
|
156
|
+
/**
|
157
|
+
* @param type Event type.
|
158
|
+
* @param listener Listener to attach to all of the `trigger` elements.
|
159
|
+
* @param options
|
160
|
+
*/
|
161
|
+
listener<T extends keyof HTMLElementEventMap>(
|
162
|
+
type: T,
|
163
|
+
listener: Listener<T>,
|
164
|
+
options?: AddEventListenerOptions,
|
165
|
+
): void;
|
166
|
+
listener<T extends keyof HTMLElementEventMap>(
|
167
|
+
listenerOrType: Listener<T> | T,
|
168
|
+
listenerOrOptions?: Listener<T> | AddEventListenerOptions,
|
169
|
+
optionsMaybe?: AddEventListenerOptions,
|
170
|
+
): void {
|
171
|
+
let type: keyof HTMLElementEventMap;
|
172
|
+
let listener: Listener<any>;
|
173
|
+
let options: AddEventListenerOptions | undefined;
|
174
|
+
|
175
|
+
if (typeof listenerOrType === "function") {
|
176
|
+
// (listener, options?)
|
177
|
+
type = this.event;
|
178
|
+
listener = listenerOrType;
|
179
|
+
options = listenerOrOptions as AddEventListenerOptions;
|
180
|
+
} else {
|
181
|
+
// (type, listener, options?)
|
182
|
+
type = listenerOrType;
|
183
|
+
listener = listenerOrOptions as Listener<T>;
|
184
|
+
options = optionsMaybe;
|
185
|
+
}
|
186
|
+
|
187
|
+
for (const trigger of this.triggers()) {
|
188
|
+
trigger.addEventListener(type, listener, options);
|
189
|
+
}
|
190
|
+
}
|
191
|
+
};
|
192
|
+
|
193
|
+
/**
|
194
|
+
* By default, `content` is selected via the `data-content` attribute.
|
195
|
+
* Alternatively, you can set the `trigger` to a CSS selector to change
|
196
|
+
* the default selector from `[data-trigger]` to a selector of your choosing.
|
197
|
+
* This can be useful if you have multiple elements within one another.
|
198
|
+
*
|
199
|
+
* Each element can only have one `content`.
|
200
|
+
*/
|
201
|
+
export const Content = <T extends Constructor<HTMLElement>>(
|
202
|
+
Super = HTMLElement as T,
|
203
|
+
) =>
|
204
|
+
class Content extends Super {
|
205
|
+
constructor(...args: any[]) {
|
206
|
+
super(...args);
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* @param instance The instance of the desired element to validate against,
|
211
|
+
* ex: `HTMLDialogElement`. Defaults to `HTMLElement`.
|
212
|
+
* @returns The element that matches the `content` selector.
|
213
|
+
* @default this.querySelector("[data-content]")
|
214
|
+
*/
|
215
|
+
content<T extends HTMLElement>(instance: Constructor<T>): T;
|
216
|
+
content(): HTMLElement;
|
217
|
+
content(instance = HTMLElement) {
|
218
|
+
return validate(
|
219
|
+
this.querySelector(this.getAttribute("content") ?? "[data-content]"),
|
220
|
+
instance,
|
221
|
+
);
|
222
|
+
}
|
223
|
+
|
224
|
+
/**
|
225
|
+
* Finds the `HTMLElement | HTMLTemplateElement` via the `swap` selector and
|
226
|
+
* swaps `this.content()` with the content of the element found.
|
227
|
+
*
|
228
|
+
* @param revert Wait time (ms) before swapping back, set to `false` to not revert.
|
229
|
+
* default: `800`
|
230
|
+
*/
|
231
|
+
swap(revert: number | false = 800) {
|
232
|
+
/** The swap element, used to hold the replacement contents. */
|
233
|
+
const swapTarget = this.querySelector(
|
234
|
+
this.getAttribute("swap") ?? "[data-swap]",
|
235
|
+
);
|
236
|
+
|
237
|
+
if (swapTarget) {
|
238
|
+
/** A copy of the content currently in `this.getContent()`. */
|
239
|
+
const currentContent = this.content().childNodes;
|
240
|
+
|
241
|
+
/**
|
242
|
+
* The contents of the swap element, set based on whether the
|
243
|
+
* swap is a `template` or not.
|
244
|
+
*/
|
245
|
+
const placeholder: Node[] = [];
|
246
|
+
|
247
|
+
// Set the placeholder with the `swap` content, then replace the
|
248
|
+
// swap content with the `currentContent`
|
249
|
+
if (swapTarget instanceof HTMLTemplateElement) {
|
250
|
+
// use `content` since it's a `template` element
|
251
|
+
placeholder.push(swapTarget.content.cloneNode(true));
|
252
|
+
swapTarget.content.replaceChildren(...currentContent);
|
253
|
+
} else {
|
254
|
+
// not a `template`, replace children directly
|
255
|
+
placeholder.push(...swapTarget.childNodes);
|
256
|
+
swapTarget.replaceChildren(...currentContent);
|
257
|
+
}
|
258
|
+
|
259
|
+
// finally, set the content to the contents of the placeholder
|
260
|
+
this.content().replaceChildren(...placeholder);
|
261
|
+
|
262
|
+
if (revert) {
|
263
|
+
// wait and then run again to swap back
|
264
|
+
setTimeout(() => this.swap(0), revert);
|
265
|
+
}
|
266
|
+
}
|
267
|
+
}
|
268
|
+
};
|
269
|
+
|
270
|
+
export const Announce = <T extends Constructor<HTMLElement>>(
|
271
|
+
Super = HTMLElement as T,
|
272
|
+
) =>
|
273
|
+
class Announce extends Super {
|
274
|
+
/**
|
275
|
+
* A single `Announcer` element to share between all drab elements to announce
|
276
|
+
* interactive changes.
|
277
|
+
*/
|
278
|
+
static #announcer = Announcer.init();
|
279
|
+
|
280
|
+
constructor(...args: any[]) {
|
281
|
+
super(...args);
|
282
|
+
}
|
283
|
+
|
284
|
+
/**
|
285
|
+
* @param message message to announce to screen readers
|
286
|
+
*/
|
287
|
+
announce(message: string) {
|
288
|
+
Announce.#announcer.announce(message);
|
289
|
+
}
|
290
|
+
};
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import {
|
2
|
+
Content,
|
3
|
+
type ContentAttributes,
|
4
|
+
Lifecycle,
|
5
|
+
Trigger,
|
6
|
+
type TriggerAttributes,
|
7
|
+
} from "../base/index.js";
|
8
|
+
|
9
|
+
export interface ContextMenuAttributes
|
10
|
+
extends TriggerAttributes,
|
11
|
+
ContentAttributes {}
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Displays content when the `trigger` element is right clicked, or long pressed on mobile.
|
15
|
+
*/
|
16
|
+
export class ContextMenu extends Lifecycle(Trigger(Content())) {
|
17
|
+
/** Tracks the long press duration on mobile. */
|
18
|
+
#touchTimer: NodeJS.Timeout | undefined;
|
19
|
+
|
20
|
+
constructor() {
|
21
|
+
super();
|
22
|
+
}
|
23
|
+
|
24
|
+
/** Sets the context menu's `style.left` and `style.top` position. */
|
25
|
+
set #coordinates(value: { x: number; y: number }) {
|
26
|
+
this.content().style.left = `${value.x}px`;
|
27
|
+
this.content().style.top = `${value.y}px`;
|
28
|
+
}
|
29
|
+
|
30
|
+
show(e: MouseEvent | TouchEvent) {
|
31
|
+
// find coordinates of the click
|
32
|
+
const scrollY = window.scrollY;
|
33
|
+
const scrollX = window.scrollX;
|
34
|
+
|
35
|
+
const clientX =
|
36
|
+
e instanceof MouseEvent ? e.clientX : (e.touches[0]?.clientX ?? 0);
|
37
|
+
const clientY =
|
38
|
+
e instanceof MouseEvent ? e.clientY : (e.touches[0]?.clientY ?? 0);
|
39
|
+
|
40
|
+
let x = clientX + scrollX;
|
41
|
+
let y = clientY + scrollY;
|
42
|
+
|
43
|
+
this.content().style.position = "absolute";
|
44
|
+
this.content().setAttribute("data-open", "");
|
45
|
+
|
46
|
+
const offsetWidth = this.content().offsetWidth + 24;
|
47
|
+
const offsetHeight = this.content().offsetHeight + 6;
|
48
|
+
const innerWidth = window.innerWidth;
|
49
|
+
const innerHeight = window.innerHeight;
|
50
|
+
|
51
|
+
// ensure menu is within view
|
52
|
+
if (x + offsetWidth > scrollX + innerWidth) {
|
53
|
+
x = scrollX + innerWidth - offsetWidth;
|
54
|
+
}
|
55
|
+
if (y + offsetHeight > scrollY + innerHeight) {
|
56
|
+
y = scrollY + innerHeight - offsetHeight;
|
57
|
+
}
|
58
|
+
|
59
|
+
this.#coordinates = { x, y };
|
60
|
+
}
|
61
|
+
|
62
|
+
hide() {
|
63
|
+
this.content().removeAttribute("data-open");
|
64
|
+
}
|
65
|
+
|
66
|
+
override mount() {
|
67
|
+
// mouse
|
68
|
+
this.listener("contextmenu", (e) => {
|
69
|
+
e.preventDefault();
|
70
|
+
this.show(e);
|
71
|
+
});
|
72
|
+
|
73
|
+
this.safeListener("click", () => this.hide());
|
74
|
+
|
75
|
+
// touch
|
76
|
+
this.listener(
|
77
|
+
"touchstart",
|
78
|
+
(e) => {
|
79
|
+
this.#touchTimer = setTimeout(() => {
|
80
|
+
this.show(e);
|
81
|
+
}, 800);
|
82
|
+
},
|
83
|
+
{ passive: true },
|
84
|
+
);
|
85
|
+
|
86
|
+
const resetTimer = () => clearTimeout(this.#touchTimer);
|
87
|
+
this.listener("touchend", resetTimer, { passive: true });
|
88
|
+
this.listener("touchcancel", resetTimer, { passive: true });
|
89
|
+
|
90
|
+
// keyboard
|
91
|
+
this.safeListener("keydown", (e) => {
|
92
|
+
if (e.key === "Escape") this.hide();
|
93
|
+
});
|
94
|
+
}
|
95
|
+
}
|
package/src/define.ts
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
import "./announcer/define.js";
|
2
|
+
import "./contextmenu/define.js";
|
3
|
+
import "./dialog/define.js";
|
4
|
+
import "./editor/define.js";
|
5
|
+
import "./fullscreen/define.js";
|
6
|
+
import "./intersect/define.js";
|
7
|
+
import "./prefetch/define.js";
|
8
|
+
import "./share/define.js";
|
9
|
+
import "./tablesort/define.js";
|
10
|
+
import "./tabs/define.js";
|
11
|
+
import "./wakelock/define.js";
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import {
|
2
|
+
Content,
|
3
|
+
type ContentAttributes,
|
4
|
+
Lifecycle,
|
5
|
+
Trigger,
|
6
|
+
type TriggerAttributes,
|
7
|
+
} from "../base/index.js";
|
8
|
+
|
9
|
+
export interface DialogAttributes extends TriggerAttributes, ContentAttributes {
|
10
|
+
/** Close the dialog when clicked outside. */
|
11
|
+
"click-outside-close"?: boolean;
|
12
|
+
|
13
|
+
/** Remove scroll from the body when dialog is open. */
|
14
|
+
"remove-body-scroll"?: boolean;
|
15
|
+
}
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Provides triggers for the `HTMLDialogElement`.
|
19
|
+
*
|
20
|
+
* ### Attributes
|
21
|
+
*
|
22
|
+
* `click-outside-close`
|
23
|
+
*
|
24
|
+
* By default, the `HTMLDialogElement` doesn't close if the user clicks outside of it.
|
25
|
+
* Add a `click-outside-close` attribute to close when the user clicks outside.
|
26
|
+
*
|
27
|
+
* If the dialog covers the full viewport and this attribute is present, the first child
|
28
|
+
* of the dialog will be assumed to be the content of the dialog and the dialog will close
|
29
|
+
* if there is a click outside of the first child element.
|
30
|
+
*
|
31
|
+
* `remove-body-scroll`
|
32
|
+
*
|
33
|
+
* Add the `remove-body-scroll` attribute to remove the scroll from `document.body` when the dialog
|
34
|
+
* is open.
|
35
|
+
*/
|
36
|
+
export class Dialog extends Lifecycle(Trigger(Content())) {
|
37
|
+
constructor() {
|
38
|
+
super();
|
39
|
+
}
|
40
|
+
|
41
|
+
/** The `HTMLDialogElement` within the element. */
|
42
|
+
get #dialog() {
|
43
|
+
return this.content(HTMLDialogElement);
|
44
|
+
}
|
45
|
+
|
46
|
+
get #removeBodyScroll() {
|
47
|
+
return this.hasAttribute("remove-body-scroll");
|
48
|
+
}
|
49
|
+
|
50
|
+
get #clickOutsideClose() {
|
51
|
+
return this.hasAttribute("click-outside-close");
|
52
|
+
}
|
53
|
+
|
54
|
+
get #scrollbarWidth() {
|
55
|
+
return window.innerWidth - document.documentElement.clientWidth;
|
56
|
+
}
|
57
|
+
|
58
|
+
/** Remove scroll from the body when open with the `remove-body-scroll` attribute. */
|
59
|
+
#toggleBodyScroll(show: boolean) {
|
60
|
+
if (this.#removeBodyScroll) {
|
61
|
+
document.documentElement.style.scrollbarGutter = "stable";
|
62
|
+
document.body.style.overflow = show ? "hidden" : "";
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
/** Wraps `HTMLDialogElement.showModal()`. */
|
67
|
+
show() {
|
68
|
+
this.#dialog.showModal();
|
69
|
+
this.#toggleBodyScroll(true);
|
70
|
+
}
|
71
|
+
|
72
|
+
/** Wraps `HTMLDialogElement.close()`. */
|
73
|
+
close() {
|
74
|
+
this.#toggleBodyScroll(false);
|
75
|
+
this.#dialog.close();
|
76
|
+
}
|
77
|
+
|
78
|
+
/** `show` or `close` depending on the dialog's `open` attribute. */
|
79
|
+
toggle() {
|
80
|
+
if (this.#dialog.open) this.close();
|
81
|
+
else this.show();
|
82
|
+
}
|
83
|
+
|
84
|
+
override mount() {
|
85
|
+
this.listener(() => this.toggle());
|
86
|
+
|
87
|
+
this.safeListener("keydown", (e) => {
|
88
|
+
if (e.key === "Escape" && this.#dialog.open) {
|
89
|
+
e.preventDefault();
|
90
|
+
this.close();
|
91
|
+
}
|
92
|
+
});
|
93
|
+
|
94
|
+
if (this.#clickOutsideClose) {
|
95
|
+
// https://blog.webdevsimplified.com/2023-04/html-dialog/#close-on-outside-click
|
96
|
+
this.#dialog.addEventListener("click", (e) => {
|
97
|
+
let rect = this.#dialog.getBoundingClientRect();
|
98
|
+
|
99
|
+
// If dialog covers full viewport, use first child element for hit testing
|
100
|
+
// Example: https://picocss.com/docs/modal
|
101
|
+
if (
|
102
|
+
Math.abs(rect.width - window.innerWidth) <= this.#scrollbarWidth && // 5px tolerance for rounding issues
|
103
|
+
Math.abs(rect.height - window.innerHeight) <= 0 &&
|
104
|
+
this.#dialog.firstElementChild
|
105
|
+
) {
|
106
|
+
rect = this.#dialog.firstElementChild.getBoundingClientRect();
|
107
|
+
}
|
108
|
+
|
109
|
+
if (
|
110
|
+
e.clientX < rect.left ||
|
111
|
+
e.clientX > rect.right ||
|
112
|
+
e.clientY < rect.top ||
|
113
|
+
e.clientY > rect.bottom
|
114
|
+
) {
|
115
|
+
this.close();
|
116
|
+
}
|
117
|
+
});
|
118
|
+
}
|
119
|
+
}
|
120
|
+
}
|