drab 7.0.0 → 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/dialog/index.js +6 -3
- 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,195 @@
|
|
1
|
+
import { Lifecycle, Trigger, type TriggerAttributes } from "../base/index.js";
|
2
|
+
|
3
|
+
type Strategy = "hover" | "load" | "visible";
|
4
|
+
|
5
|
+
export interface PrefetchAttributes extends TriggerAttributes {
|
6
|
+
/** When to prefetch the url. */
|
7
|
+
strategy?: Strategy;
|
8
|
+
|
9
|
+
/** Prerender on the client with the Speculation Rules API. */
|
10
|
+
prerender?: boolean;
|
11
|
+
|
12
|
+
/** URL to prefetch. */
|
13
|
+
url?: string;
|
14
|
+
}
|
15
|
+
|
16
|
+
// https://developer.chrome.com/blog/speculation-rules-improvements
|
17
|
+
type SpeculationRules = {
|
18
|
+
prerender?: DocumentRule[] | ListRule[];
|
19
|
+
prefetch?: DocumentRule[] | ListRule[];
|
20
|
+
};
|
21
|
+
|
22
|
+
type Rule = {
|
23
|
+
expects_no_vary_search?: string;
|
24
|
+
referrer_policy?: ReferrerPolicy;
|
25
|
+
requires?: string[];
|
26
|
+
};
|
27
|
+
|
28
|
+
type DocumentRule = {
|
29
|
+
source: "document";
|
30
|
+
where: WhereCondition;
|
31
|
+
eagerness?: "immediate" | "moderate" | "eager" | "conservative";
|
32
|
+
} & Rule;
|
33
|
+
|
34
|
+
type ListRule = { source: "list"; urls: string[] } & Rule;
|
35
|
+
|
36
|
+
type WhereCondition =
|
37
|
+
| { href_matches: string }
|
38
|
+
| { selector_matches: string }
|
39
|
+
| { and: WhereCondition[] }
|
40
|
+
| { not: WhereCondition }
|
41
|
+
| { or: WhereCondition[] };
|
42
|
+
|
43
|
+
/**
|
44
|
+
* The `Prefetch` element can prefetch a url, or enhance the `HTMLAnchorElement` by loading the HTML for a page before it is navigated to. This element speeds up the navigation for multi-page applications (MPAs).
|
45
|
+
*
|
46
|
+
* If you are using a framework that already has a prefetch feature or uses a client side router, it is best to use the framework's feature instead of this element to ensure prefetching is working in accordance with the router.
|
47
|
+
*
|
48
|
+
* ### Attributes
|
49
|
+
*
|
50
|
+
* `strategy`
|
51
|
+
*
|
52
|
+
* Set the `strategy` attribute to specify the when the prefetch will take place.
|
53
|
+
*
|
54
|
+
* - `"hover"` - (default) After `mouseover`, `focus`, or `touchstart` for > 200ms
|
55
|
+
* - `"visible"` - Uses an intersection observer to prefetch when the anchor is within the viewport.
|
56
|
+
* - `"load"` - Immediately prefetches when the element is loaded, use carefully.
|
57
|
+
*
|
58
|
+
* `prerender`
|
59
|
+
*
|
60
|
+
* Use the `prerender` attribute to use the Speculation Rules API when supported to prerender on the client. This allows you to run client side JavaScript in advance instead of only fetching the HTML.
|
61
|
+
*
|
62
|
+
* Browsers that do not support will still use `<link rel="prefetch">` instead.
|
63
|
+
*
|
64
|
+
* [Speculation Rules Reference](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API)
|
65
|
+
*
|
66
|
+
* `url`
|
67
|
+
*
|
68
|
+
* Add a `url` attribute to immediately prefetch a url without having to provide
|
69
|
+
* (or in addition to) `trigger` anchor elements.
|
70
|
+
*
|
71
|
+
* This element can be deprecated once the Speculation Rules API is supported across browsers. The API will be able to prefetch assets in a similar way with the `source: "document"` and `eagerness` features, and will work without JavaScript.
|
72
|
+
*/
|
73
|
+
export class Prefetch extends Lifecycle(Trigger()) {
|
74
|
+
#prefetchedUrls = new Set<string>();
|
75
|
+
|
76
|
+
constructor() {
|
77
|
+
super();
|
78
|
+
}
|
79
|
+
|
80
|
+
/** When to prefetch the url. */
|
81
|
+
get #strategy() {
|
82
|
+
return this.getAttribute("strategy");
|
83
|
+
}
|
84
|
+
|
85
|
+
/** Prerender with the Speculation Rules API. */
|
86
|
+
get #prerender() {
|
87
|
+
return this.hasAttribute("prerender");
|
88
|
+
}
|
89
|
+
|
90
|
+
/** `url` to prefetch on `mount`. */
|
91
|
+
get #url() {
|
92
|
+
return this.getAttribute("url");
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Appends `<link rel="prefetch">` or `<script type="speculationrules">`
|
97
|
+
* to the head of the document.
|
98
|
+
*
|
99
|
+
* @param options Configuration options.
|
100
|
+
*/
|
101
|
+
prefetch(options: {
|
102
|
+
/** `url` to prefetch. */
|
103
|
+
url: string;
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Uses the Speculation Rules API when supported to prerender on the client.
|
107
|
+
*/
|
108
|
+
prerender?: boolean;
|
109
|
+
}) {
|
110
|
+
const { url } = options;
|
111
|
+
|
112
|
+
// if not the current page and not already prefetched
|
113
|
+
if (!(url === window.location.href) && !this.#prefetchedUrls.has(url)) {
|
114
|
+
this.#prefetchedUrls.add(url);
|
115
|
+
|
116
|
+
if (
|
117
|
+
HTMLScriptElement.supports &&
|
118
|
+
HTMLScriptElement.supports("speculationrules")
|
119
|
+
) {
|
120
|
+
const rules: SpeculationRules = {
|
121
|
+
// Currently, adding `prefetch` is required to fallback if `prerender` fails.
|
122
|
+
// Possibly will be automatic in the future, in which case it can be removed.
|
123
|
+
// https://github.com/WICG/nav-speculation/issues/162#issuecomment-1977818473
|
124
|
+
prefetch: [{ source: "list", urls: [url] }],
|
125
|
+
};
|
126
|
+
|
127
|
+
if (options.prerender) rules.prerender = rules.prefetch;
|
128
|
+
|
129
|
+
const script = document.createElement("script");
|
130
|
+
script.type = "speculationrules";
|
131
|
+
script.textContent = JSON.stringify(rules);
|
132
|
+
document.head.append(script);
|
133
|
+
} else {
|
134
|
+
const link = document.createElement("link");
|
135
|
+
link.rel = "prefetch";
|
136
|
+
link.as = "document";
|
137
|
+
link.href = url;
|
138
|
+
document.head.append(link);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
override mount() {
|
144
|
+
// immediately prefetch the `url` attribute if it exists
|
145
|
+
if (this.#url)
|
146
|
+
this.prefetch({ url: this.#url, prerender: this.#prerender });
|
147
|
+
|
148
|
+
// prefetch the `trigger` elements
|
149
|
+
const anchors = this.triggers(HTMLAnchorElement);
|
150
|
+
const prerender = this.#prerender;
|
151
|
+
const strategy = this.#strategy;
|
152
|
+
|
153
|
+
let prefetchTimer: NodeJS.Timeout;
|
154
|
+
|
155
|
+
const listener =
|
156
|
+
(delay = 200) =>
|
157
|
+
(e: Event) => {
|
158
|
+
const { href } = e.currentTarget as HTMLAnchorElement;
|
159
|
+
prefetchTimer = setTimeout(
|
160
|
+
() => this.prefetch({ url: href, prerender }),
|
161
|
+
delay,
|
162
|
+
);
|
163
|
+
};
|
164
|
+
|
165
|
+
const reset = () => clearTimeout(prefetchTimer);
|
166
|
+
|
167
|
+
const observer = new IntersectionObserver((entries) => {
|
168
|
+
for (const entry of entries) {
|
169
|
+
if (entry.isIntersecting) {
|
170
|
+
this.prefetch({
|
171
|
+
url: (entry.target as HTMLAnchorElement).href,
|
172
|
+
prerender,
|
173
|
+
});
|
174
|
+
}
|
175
|
+
}
|
176
|
+
});
|
177
|
+
|
178
|
+
for (const anchor of anchors) {
|
179
|
+
if (strategy === "load") {
|
180
|
+
this.prefetch({ url: anchor.href, prerender });
|
181
|
+
} else if (strategy === "visible") {
|
182
|
+
observer.observe(anchor);
|
183
|
+
} else {
|
184
|
+
// "hover" - default
|
185
|
+
anchor.addEventListener("mouseover", listener());
|
186
|
+
anchor.addEventListener("mouseout", reset);
|
187
|
+
anchor.addEventListener("focus", listener());
|
188
|
+
anchor.addEventListener("focusout", reset);
|
189
|
+
// immediately append on touchstart, no delay
|
190
|
+
// passive: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners
|
191
|
+
anchor.addEventListener("touchstart", listener(0), { passive: true });
|
192
|
+
}
|
193
|
+
}
|
194
|
+
}
|
195
|
+
}
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import {
|
2
|
+
Announce,
|
3
|
+
Content,
|
4
|
+
type ContentAttributes,
|
5
|
+
Lifecycle,
|
6
|
+
Trigger,
|
7
|
+
type TriggerAttributes,
|
8
|
+
} from "../base/index.js";
|
9
|
+
|
10
|
+
export type ShareAttributes = TriggerAttributes &
|
11
|
+
ContentAttributes &
|
12
|
+
(
|
13
|
+
| {
|
14
|
+
/** Share URL */
|
15
|
+
url: string;
|
16
|
+
|
17
|
+
/** `ShareData` text (only supported on some targets) */
|
18
|
+
text?: string;
|
19
|
+
|
20
|
+
/** Share title */
|
21
|
+
"share-title"?: string;
|
22
|
+
}
|
23
|
+
| {
|
24
|
+
/** Text to copy */
|
25
|
+
text: string;
|
26
|
+
}
|
27
|
+
);
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Uses the
|
31
|
+
* [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
|
32
|
+
* to share the `url` if `navigator.share` is supported.
|
33
|
+
*
|
34
|
+
* Otherwise uses the
|
35
|
+
* [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)
|
36
|
+
* to copy the `url` or `text` provided.
|
37
|
+
*
|
38
|
+
* ### Attributes
|
39
|
+
*
|
40
|
+
* `url`
|
41
|
+
*
|
42
|
+
* URL to share.
|
43
|
+
*
|
44
|
+
* `text`
|
45
|
+
*
|
46
|
+
* Text to copy, or the `ShareData` text if `url` is set (only supported on some targets).
|
47
|
+
*
|
48
|
+
* `share-title`
|
49
|
+
*
|
50
|
+
* `ShareData` title (only supported on some targets).
|
51
|
+
*/
|
52
|
+
export class Share extends Lifecycle(Trigger(Content(Announce()))) {
|
53
|
+
constructor() {
|
54
|
+
super();
|
55
|
+
}
|
56
|
+
|
57
|
+
// helper since ShareData expects undefined instead of null
|
58
|
+
#attrOrUndefined(name: string) {
|
59
|
+
return this.getAttribute(name) ?? undefined;
|
60
|
+
}
|
61
|
+
|
62
|
+
get #title() {
|
63
|
+
return this.#attrOrUndefined("share-title");
|
64
|
+
}
|
65
|
+
|
66
|
+
get #text() {
|
67
|
+
return this.#attrOrUndefined("text");
|
68
|
+
}
|
69
|
+
|
70
|
+
get #url() {
|
71
|
+
return this.#attrOrUndefined("url");
|
72
|
+
}
|
73
|
+
|
74
|
+
override mount() {
|
75
|
+
this.listener(() => {
|
76
|
+
const data: ShareData = {
|
77
|
+
title: this.#title,
|
78
|
+
text: this.#text,
|
79
|
+
url: this.#url,
|
80
|
+
};
|
81
|
+
|
82
|
+
if (data.url && navigator.canShare && navigator.canShare(data)) {
|
83
|
+
return navigator.share(data).catch((e) => {
|
84
|
+
// catch abort errors when user cancels the share
|
85
|
+
if (!(e instanceof Error) || e.name !== "AbortError") throw e;
|
86
|
+
});
|
87
|
+
}
|
88
|
+
|
89
|
+
const copy = data.url || data.text;
|
90
|
+
|
91
|
+
if (copy) {
|
92
|
+
return navigator.clipboard.writeText(copy).then(() => {
|
93
|
+
this.announce("copied to clipboard");
|
94
|
+
this.swap();
|
95
|
+
});
|
96
|
+
}
|
97
|
+
});
|
98
|
+
}
|
99
|
+
}
|
@@ -0,0 +1,168 @@
|
|
1
|
+
import {
|
2
|
+
Announce,
|
3
|
+
Content,
|
4
|
+
type ContentAttributes,
|
5
|
+
Lifecycle,
|
6
|
+
Trigger,
|
7
|
+
type TriggerAttributes,
|
8
|
+
} from "../base/index.js";
|
9
|
+
|
10
|
+
export interface TableSortAttributes
|
11
|
+
extends TriggerAttributes,
|
12
|
+
ContentAttributes {}
|
13
|
+
|
14
|
+
export interface TableSortTriggerAttributes {
|
15
|
+
"data-type": "string" | "boolean" | "number";
|
16
|
+
"data-value": string;
|
17
|
+
}
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Wrap a `HTMLTableElement` in the `TableSort` element to have sortable column
|
21
|
+
* headers. Set each `th` that you want to sort to the `trigger`. Set the `tbody`
|
22
|
+
* element to the `content`.
|
23
|
+
*
|
24
|
+
* The values of each cell default to the cell's `textContent`. If you would like to
|
25
|
+
* provide an alternate value than what appears in the cell to sort by instead,
|
26
|
+
* you can set a different value using the `data-value` attribute on the cell.
|
27
|
+
*
|
28
|
+
* The cells will be sorted as `string` by default. If you want to provide a different
|
29
|
+
* datatype `number` or `boolean`, set `data-type="number"` on the corresponding
|
30
|
+
* `th`/`trigger` element. The data will be converted to the specified type before sorting.
|
31
|
+
*/
|
32
|
+
export class TableSort extends Lifecycle(Trigger(Content(Announce()))) {
|
33
|
+
constructor() {
|
34
|
+
super();
|
35
|
+
}
|
36
|
+
|
37
|
+
get #th() {
|
38
|
+
return this.triggers(HTMLTableCellElement);
|
39
|
+
}
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Removes `data-asc` or `data-desc` from other triggers then sets the correct attribute on the selected trigger.
|
43
|
+
*
|
44
|
+
* @param trigger
|
45
|
+
* @returns `true` if ascending, `false` if descending
|
46
|
+
*/
|
47
|
+
#setAttributes(trigger: HTMLElement) {
|
48
|
+
const asc = "data-asc";
|
49
|
+
const desc = "data-desc";
|
50
|
+
|
51
|
+
for (const t of this.triggers(HTMLTableCellElement)) {
|
52
|
+
if (t !== trigger) {
|
53
|
+
t.removeAttribute(asc);
|
54
|
+
t.removeAttribute(desc);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
if (trigger.hasAttribute(asc)) {
|
59
|
+
trigger.removeAttribute(asc);
|
60
|
+
trigger.setAttribute(desc, "");
|
61
|
+
return false;
|
62
|
+
}
|
63
|
+
|
64
|
+
trigger.removeAttribute(desc);
|
65
|
+
trigger.setAttribute(asc, "");
|
66
|
+
|
67
|
+
return true;
|
68
|
+
}
|
69
|
+
|
70
|
+
override mount() {
|
71
|
+
const tbody = this.content(HTMLTableSectionElement);
|
72
|
+
|
73
|
+
for (const trigger of this.#th) {
|
74
|
+
trigger.tabIndex = 0;
|
75
|
+
trigger.role = "button";
|
76
|
+
|
77
|
+
const listener = () => {
|
78
|
+
const asc = this.#setAttributes(trigger);
|
79
|
+
|
80
|
+
Array.from(tbody.querySelectorAll("tr"))
|
81
|
+
.sort(comparer(trigger, asc))
|
82
|
+
.forEach((tr) => tbody.appendChild(tr));
|
83
|
+
|
84
|
+
this.announce(
|
85
|
+
`sorted table by ${trigger.textContent} in ${asc ? "ascending" : "descending"} order`,
|
86
|
+
);
|
87
|
+
};
|
88
|
+
|
89
|
+
trigger.addEventListener(this.event, listener);
|
90
|
+
|
91
|
+
if (this.event === "click") {
|
92
|
+
trigger.addEventListener("keydown", (e) => {
|
93
|
+
if (e.key === "Enter" || e.key === " ") {
|
94
|
+
e.preventDefault();
|
95
|
+
listener();
|
96
|
+
}
|
97
|
+
});
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
// adapted from: https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript/49041392#49041392
|
104
|
+
const comparer = (th: HTMLElement, ascending: boolean) => {
|
105
|
+
// this function is returned and used by `sort`
|
106
|
+
const sorter = (a: HTMLTableRowElement, b: HTMLTableRowElement) => {
|
107
|
+
// find the column to sort by using the index of the `th`
|
108
|
+
const columnIndex = Array.from(th.parentNode?.children ?? []).indexOf(th);
|
109
|
+
|
110
|
+
const compare = (aVal: string, bVal: string) => {
|
111
|
+
// default to `string` sorting
|
112
|
+
const dataType = (th.dataset.type ?? "string") as
|
113
|
+
| "string"
|
114
|
+
| "boolean"
|
115
|
+
| "number";
|
116
|
+
|
117
|
+
if (dataType === "string") {
|
118
|
+
const collator = new Intl.Collator();
|
119
|
+
return collator.compare(aVal, bVal);
|
120
|
+
} else if (dataType === "boolean") {
|
121
|
+
return falsyBoolean(aVal) === falsyBoolean(bVal)
|
122
|
+
? 0
|
123
|
+
: falsyBoolean(aVal)
|
124
|
+
? -1
|
125
|
+
: 1;
|
126
|
+
} else {
|
127
|
+
// "number"
|
128
|
+
return Number(aVal) - Number(bVal);
|
129
|
+
}
|
130
|
+
};
|
131
|
+
|
132
|
+
return compare(
|
133
|
+
getValue(ascending ? a : b, columnIndex),
|
134
|
+
getValue(ascending ? b : a, columnIndex),
|
135
|
+
);
|
136
|
+
};
|
137
|
+
|
138
|
+
return sorter;
|
139
|
+
};
|
140
|
+
|
141
|
+
/**
|
142
|
+
* @param tr the row
|
143
|
+
* @param i index of the `td` to find
|
144
|
+
* @returns a string, the `data-value` attribute, or the `textContent`
|
145
|
+
*/
|
146
|
+
const getValue = (tr: HTMLTableRowElement, i: number) => {
|
147
|
+
const cell = tr.children[i];
|
148
|
+
if (cell instanceof HTMLElement) {
|
149
|
+
// first look for `data-value` attribute, then use `textContent`
|
150
|
+
return cell.dataset.value ?? cell.textContent ?? "";
|
151
|
+
}
|
152
|
+
return "";
|
153
|
+
};
|
154
|
+
|
155
|
+
/**
|
156
|
+
* if value is one of these and type is boolean
|
157
|
+
* it should be considered falsy
|
158
|
+
* since actually `Boolean("false") === true`
|
159
|
+
* @param val string pulled from the textContent or attr
|
160
|
+
* @returns a boolean of the provided string
|
161
|
+
*/
|
162
|
+
const falsyBoolean = (val: string) => {
|
163
|
+
if (["0", "false", "null", "undefined"].includes(val)) {
|
164
|
+
return false;
|
165
|
+
}
|
166
|
+
|
167
|
+
return Boolean(val);
|
168
|
+
};
|
@@ -0,0 +1,173 @@
|
|
1
|
+
import { Lifecycle, Trigger, type TriggerAttributes } from "../base/index.js";
|
2
|
+
|
3
|
+
export interface TabsAttributes extends TriggerAttributes {
|
4
|
+
/** Set to `"vertical"` if tabs are displayed vertically. */
|
5
|
+
orientation?: "horizontal" | "vertical";
|
6
|
+
}
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Progressively enhance a list of links and content to be tabs by
|
10
|
+
* wrapping with the `Tabs` element. Each `trigger` should be an
|
11
|
+
* `HTMLAnchorElement` with the `href` attribute set to the `id` of the
|
12
|
+
* corresponding tab panel.
|
13
|
+
*
|
14
|
+
* > Tip: Set the `height` of the element the `panel`s are contained in with
|
15
|
+
* > CSS to prevent layout shift when JS is loaded.
|
16
|
+
*
|
17
|
+
* This element is based on
|
18
|
+
* [Chris Ferdinandi's Toggle Tabs](https://gomakethings.com/a-web-component-ui-library-for-people-who-love-html/#toggle-tabs)
|
19
|
+
* design.
|
20
|
+
*
|
21
|
+
* [ARIA Reference](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
|
22
|
+
*
|
23
|
+
* - Sets the correct ARIA attributes on each element.
|
24
|
+
* - Supports keyboard navigation based on the `orientation` attribute.
|
25
|
+
* - First tab is selected by default if no `aria-selected="true"` attribute is
|
26
|
+
* found on another tab.
|
27
|
+
* - `tablist` is calculated based on the deepest common parent of the tabs,
|
28
|
+
* throws an error if not found.
|
29
|
+
*
|
30
|
+
* ### Attributes
|
31
|
+
*
|
32
|
+
* `orientation`
|
33
|
+
*
|
34
|
+
* Set `orientation="vertical"` if the `tablist` element is displayed vertically.
|
35
|
+
*/
|
36
|
+
export class Tabs extends Lifecycle(Trigger()) {
|
37
|
+
/** Supported keys for keyboard navigation. */
|
38
|
+
#keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
|
39
|
+
|
40
|
+
/** Currently selected tab. */
|
41
|
+
#selected: { tab?: HTMLAnchorElement; index: number } = { index: 0 };
|
42
|
+
|
43
|
+
constructor() {
|
44
|
+
super();
|
45
|
+
}
|
46
|
+
|
47
|
+
/** User provided orientation of the `tablist`. */
|
48
|
+
get #orientation() {
|
49
|
+
return this.getAttribute("orientation") ?? "horizontal";
|
50
|
+
}
|
51
|
+
|
52
|
+
get #tabs() {
|
53
|
+
return this.triggers(HTMLAnchorElement);
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* @param tab
|
58
|
+
* @returns The ancestors of the tab up to `this`.
|
59
|
+
*/
|
60
|
+
#ancestors(tab?: HTMLElement) {
|
61
|
+
const ancestors = new Set<HTMLElement>();
|
62
|
+
|
63
|
+
let current: HTMLElement | null | undefined = tab;
|
64
|
+
while ((current = current?.parentElement) && current !== this) {
|
65
|
+
ancestors.add(current);
|
66
|
+
}
|
67
|
+
|
68
|
+
return ancestors;
|
69
|
+
}
|
70
|
+
|
71
|
+
/** A map of each `tab` and its corresponding `panel`. */
|
72
|
+
get #map() {
|
73
|
+
const map = new Map<HTMLAnchorElement, HTMLElement>();
|
74
|
+
|
75
|
+
for (const tab of this.#tabs) {
|
76
|
+
const panel = this.querySelector(tab.hash);
|
77
|
+
if (!(panel instanceof HTMLElement))
|
78
|
+
throw new Error(`Tabs: No HTMLElement with ID of ${tab.hash} found.`);
|
79
|
+
|
80
|
+
map.set(tab, panel);
|
81
|
+
}
|
82
|
+
|
83
|
+
return map;
|
84
|
+
}
|
85
|
+
|
86
|
+
override mount() {
|
87
|
+
// create tablist
|
88
|
+
const [first, ...rest] = this.#tabs;
|
89
|
+
let common = this.#ancestors(first);
|
90
|
+
for (let i = 0; i < rest.length; i++) {
|
91
|
+
common = common.intersection(this.#ancestors(rest[i]));
|
92
|
+
}
|
93
|
+
const [tablist] = common;
|
94
|
+
if (!tablist)
|
95
|
+
throw new Error("Tabs: No common parent element found for triggers.");
|
96
|
+
tablist.role = "tablist";
|
97
|
+
tablist.ariaOrientation = this.#orientation;
|
98
|
+
|
99
|
+
// enhance tabs/panels
|
100
|
+
let index = 0;
|
101
|
+
for (const [tab, panel] of this.#map) {
|
102
|
+
tab.role = "tab";
|
103
|
+
tab.id = `tab-${panel.id}`;
|
104
|
+
tab.setAttribute("aria-controls", panel.id);
|
105
|
+
if (tab.ariaSelected) this.#selected = { tab, index };
|
106
|
+
|
107
|
+
panel.role = "tabpanel";
|
108
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
109
|
+
|
110
|
+
tab.addEventListener(this.event, (e) => {
|
111
|
+
e.preventDefault();
|
112
|
+
|
113
|
+
for (const [t, p] of this.#map) {
|
114
|
+
if (t === tab) {
|
115
|
+
// show current
|
116
|
+
t.ariaSelected = "true";
|
117
|
+
t.tabIndex = 0;
|
118
|
+
p.hidden = false;
|
119
|
+
} else {
|
120
|
+
// hide others
|
121
|
+
t.ariaSelected = "false";
|
122
|
+
t.tabIndex = -1;
|
123
|
+
p.hidden = true;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
});
|
127
|
+
|
128
|
+
index++;
|
129
|
+
}
|
130
|
+
|
131
|
+
// fallback to first
|
132
|
+
if (!this.#selected.tab) this.#selected.tab = this.#tabs[0];
|
133
|
+
|
134
|
+
// select the current tab
|
135
|
+
this.#selected.tab?.click();
|
136
|
+
|
137
|
+
// handle keyboard navigation
|
138
|
+
this.addEventListener("keydown", (e) => {
|
139
|
+
const i = this.#keys.indexOf(e.key);
|
140
|
+
if (i === -1) return;
|
141
|
+
|
142
|
+
const previousIndex = this.#selected.index;
|
143
|
+
const vertical = this.#orientation === "vertical";
|
144
|
+
|
145
|
+
if (
|
146
|
+
((!vertical && i === 0) || (vertical && i === 1)) &&
|
147
|
+
this.#tabs[this.#selected.index + 1]
|
148
|
+
) {
|
149
|
+
// next
|
150
|
+
this.#selected.tab = this.#tabs[++this.#selected.index];
|
151
|
+
} else if (
|
152
|
+
((!vertical && i === 2) || (vertical && i === 3)) &&
|
153
|
+
this.#tabs[this.#selected.index - 1]
|
154
|
+
) {
|
155
|
+
// previous
|
156
|
+
this.#selected.tab = this.#tabs[--this.#selected.index];
|
157
|
+
} else if (i === 4) {
|
158
|
+
// home
|
159
|
+
this.#selected = { tab: this.#tabs[0], index: 0 };
|
160
|
+
} else if (i === 5) {
|
161
|
+
// end
|
162
|
+
const index = this.#tabs.length - 1;
|
163
|
+
this.#selected = { tab: this.#tabs[index], index };
|
164
|
+
}
|
165
|
+
|
166
|
+
if (this.#selected.index === previousIndex) return;
|
167
|
+
|
168
|
+
e.preventDefault();
|
169
|
+
this.#selected.tab?.click();
|
170
|
+
this.#selected.tab?.focus();
|
171
|
+
});
|
172
|
+
}
|
173
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import type { AnnouncerAttributes } from "../announcer/index.js";
|
2
|
+
import type { ContextMenuAttributes } from "../contextmenu/index.js";
|
3
|
+
import type { DialogAttributes } from "../dialog/index.js";
|
4
|
+
import type { EditorAttributes } from "../editor/index.js";
|
5
|
+
import type { FullscreenAttributes } from "../fullscreen/index.js";
|
6
|
+
import type { IntersectAttributes } from "../intersect/index.js";
|
7
|
+
import type { PrefetchAttributes } from "../prefetch/index.js";
|
8
|
+
import type { ShareAttributes } from "../share/index.js";
|
9
|
+
import type { TableSortAttributes } from "../tablesort/index.js";
|
10
|
+
import type { TabsAttributes } from "../tabs/index.js";
|
11
|
+
import type { WakeLockAttributes } from "../wakelock/index.js";
|
12
|
+
|
13
|
+
export interface Attributes {
|
14
|
+
announcer: AnnouncerAttributes;
|
15
|
+
contextmenu: ContextMenuAttributes;
|
16
|
+
dialog: DialogAttributes;
|
17
|
+
editor: EditorAttributes;
|
18
|
+
fullscreen: FullscreenAttributes;
|
19
|
+
intersect: IntersectAttributes;
|
20
|
+
prefetch: PrefetchAttributes;
|
21
|
+
share: ShareAttributes;
|
22
|
+
tablesort: TableSortAttributes;
|
23
|
+
tabs: TabsAttributes;
|
24
|
+
wakelock: WakeLockAttributes;
|
25
|
+
}
|
26
|
+
|
27
|
+
type Prefixed<
|
28
|
+
Prefix extends string,
|
29
|
+
GlobalAttributes,
|
30
|
+
ElementAttributes extends Record<string, any>,
|
31
|
+
> = {
|
32
|
+
[K in keyof ElementAttributes as `${Prefix}-${Extract<K, string>}`]: ElementAttributes[K] &
|
33
|
+
GlobalAttributes;
|
34
|
+
};
|
35
|
+
|
36
|
+
export type Elements<
|
37
|
+
GlobalAttributes,
|
38
|
+
Prefix extends string = "drab",
|
39
|
+
> = Prefixed<Prefix, GlobalAttributes, Attributes>;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/**
|
2
|
+
* Define a custom element to the registry. Checks if the element is
|
3
|
+
* defined and then names the element.
|
4
|
+
*
|
5
|
+
* @param name name of the custom element
|
6
|
+
* @param Constructor custom element constructor
|
7
|
+
*/
|
8
|
+
export const define = (name: string, Constructor: CustomElementConstructor) => {
|
9
|
+
if (!customElements.get(name)) customElements.define(name, Constructor);
|
10
|
+
};
|