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.
Files changed (86) hide show
  1. package/dist/announcer/define.d.ts +1 -0
  2. package/dist/announcer/define.d.ts.map +1 -0
  3. package/dist/announcer/index.d.ts +1 -0
  4. package/dist/announcer/index.d.ts.map +1 -0
  5. package/dist/base/index.d.ts +1 -0
  6. package/dist/base/index.d.ts.map +1 -0
  7. package/dist/contextmenu/define.d.ts +1 -0
  8. package/dist/contextmenu/define.d.ts.map +1 -0
  9. package/dist/contextmenu/index.d.ts +2 -1
  10. package/dist/contextmenu/index.d.ts.map +1 -0
  11. package/dist/define.d.ts +1 -0
  12. package/dist/define.d.ts.map +1 -0
  13. package/dist/dialog/define.d.ts +1 -0
  14. package/dist/dialog/define.d.ts.map +1 -0
  15. package/dist/dialog/index.d.ts +2 -1
  16. package/dist/dialog/index.d.ts.map +1 -0
  17. package/dist/dialog/index.js +6 -3
  18. package/dist/editor/define.d.ts +1 -0
  19. package/dist/editor/define.d.ts.map +1 -0
  20. package/dist/editor/index.d.ts +2 -1
  21. package/dist/editor/index.d.ts.map +1 -0
  22. package/dist/fullscreen/define.d.ts +1 -0
  23. package/dist/fullscreen/define.d.ts.map +1 -0
  24. package/dist/fullscreen/index.d.ts +2 -1
  25. package/dist/fullscreen/index.d.ts.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/intersect/define.d.ts +1 -0
  29. package/dist/intersect/define.d.ts.map +1 -0
  30. package/dist/intersect/index.d.ts +2 -1
  31. package/dist/intersect/index.d.ts.map +1 -0
  32. package/dist/prefetch/define.d.ts +1 -0
  33. package/dist/prefetch/define.d.ts.map +1 -0
  34. package/dist/prefetch/index.d.ts +1 -0
  35. package/dist/prefetch/index.d.ts.map +1 -0
  36. package/dist/share/define.d.ts +1 -0
  37. package/dist/share/define.d.ts.map +1 -0
  38. package/dist/share/index.d.ts +2 -1
  39. package/dist/share/index.d.ts.map +1 -0
  40. package/dist/tablesort/define.d.ts +1 -0
  41. package/dist/tablesort/define.d.ts.map +1 -0
  42. package/dist/tablesort/index.d.ts +2 -1
  43. package/dist/tablesort/index.d.ts.map +1 -0
  44. package/dist/tabs/define.d.ts +1 -0
  45. package/dist/tabs/define.d.ts.map +1 -0
  46. package/dist/tabs/index.d.ts +1 -0
  47. package/dist/tabs/index.d.ts.map +1 -0
  48. package/dist/types/index.d.ts +1 -0
  49. package/dist/types/index.d.ts.map +1 -0
  50. package/dist/util/define.d.ts +1 -0
  51. package/dist/util/define.d.ts.map +1 -0
  52. package/dist/util/validate.d.ts +1 -0
  53. package/dist/util/validate.d.ts.map +1 -0
  54. package/dist/wakelock/define.d.ts +1 -0
  55. package/dist/wakelock/define.d.ts.map +1 -0
  56. package/dist/wakelock/index.d.ts +2 -1
  57. package/dist/wakelock/index.d.ts.map +1 -0
  58. package/package.json +4 -2
  59. package/src/announcer/define.ts +4 -0
  60. package/src/announcer/index.ts +93 -0
  61. package/src/base/index.ts +290 -0
  62. package/src/contextmenu/define.ts +4 -0
  63. package/src/contextmenu/index.ts +95 -0
  64. package/src/define.ts +11 -0
  65. package/src/dialog/define.ts +4 -0
  66. package/src/dialog/index.ts +120 -0
  67. package/src/editor/define.ts +4 -0
  68. package/src/editor/index.ts +448 -0
  69. package/src/fullscreen/define.ts +4 -0
  70. package/src/fullscreen/index.ts +59 -0
  71. package/src/index.ts +11 -0
  72. package/src/intersect/define.ts +4 -0
  73. package/src/intersect/index.ts +79 -0
  74. package/src/prefetch/define.ts +4 -0
  75. package/src/prefetch/index.ts +195 -0
  76. package/src/share/define.ts +4 -0
  77. package/src/share/index.ts +99 -0
  78. package/src/tablesort/define.ts +4 -0
  79. package/src/tablesort/index.ts +168 -0
  80. package/src/tabs/define.ts +4 -0
  81. package/src/tabs/index.ts +173 -0
  82. package/src/types/index.ts +39 -0
  83. package/src/util/define.ts +10 -0
  84. package/src/util/validate.ts +16 -0
  85. package/src/wakelock/define.ts +4 -0
  86. package/src/wakelock/index.ts +133 -0
@@ -0,0 +1,448 @@
1
+ import {
2
+ Content,
3
+ type ContentAttributes,
4
+ Lifecycle,
5
+ Trigger,
6
+ type TriggerAttributes,
7
+ } from "../base/index.js";
8
+
9
+ export interface EditorAttributes
10
+ extends TriggerAttributes,
11
+ ContentAttributes {}
12
+
13
+ export interface EditorTriggerAttributes {
14
+ "data-value": string;
15
+ "data-key": string;
16
+ "data-type": "block" | "wrap" | "inline";
17
+ }
18
+
19
+ /**
20
+ * A piece of content to insert into the `textarea`.
21
+ */
22
+ export type ContentElement = {
23
+ /** How to insert the content. */
24
+ type: "block" | "inline" | "wrap";
25
+
26
+ /** The value to insert. */
27
+ value: string;
28
+
29
+ /** An optional keyboard shortcut. */
30
+ key?: string;
31
+ };
32
+
33
+ /**
34
+ * Enhances the `textarea` element with controls to add content and keyboard shortcuts. Compared to other WYSIWYG editors, the `text` value is just a `string`, so you can easily store it in a database or manipulate it without learning a separate API.
35
+ *
36
+ * - Automatically adds closing characters for `keyPairs`. For example, when
37
+ * typing `(`, `)` will be inserted and typed over when reached. All content
38
+ * with `data-type="wrap"` is also added to `keyPairs`.
39
+ * - Highlights the first word of the text inserted if it contains letters.
40
+ * - Automatically increments/decrements ordered lists.
41
+ * - Adds the starting character to the next line for `block` content.
42
+ * - On double click, highlight is corrected to only highlight the current word
43
+ * without space around it.
44
+ * - `tab` key will indent or dedent (+shift) instead of focus change if the
45
+ * selection is within a code block (three backticks).
46
+ * - When text is highlighted and a `wrap` character `keyPair` is typed, the
47
+ * highlighted text will be wrapped with the character instead of removing it.
48
+ * For example, if a word is highlighted and the `"` character is typed, the
49
+ * work will be surrounded by `"`s.
50
+ *
51
+ * ### Trigger attributes
52
+ *
53
+ * `data-value`
54
+ *
55
+ * Set the value of the text to be inserted using the `data-value` attribute on the `trigger`.
56
+ *
57
+ * `data-type`
58
+ *
59
+ * Set the `data-type` attribute of the `trigger` to specify how the content should be inserted into the `textarea`.
60
+ *
61
+ * - `block` will be inserted at the beginning of the selected line.
62
+ * - `wrap` will be inserted before and after the current selection.
63
+ * - `inline` will be inserted at the current selection.
64
+ *
65
+ * `data-key`
66
+ *
67
+ * Add a `ctrl`/`meta` keyboard shortcut for the content based on the `data-key` attribute.
68
+ *
69
+ */
70
+ export class Editor extends Lifecycle(Trigger(Content())) {
71
+ /** Array of `keyPair` characters that have been opened. */
72
+ #openChars: string[] = [];
73
+
74
+ /** Keys that will reset the type over for keyPairs */
75
+ #resetKeys = new Set(["ArrowUp", "ArrowDown", "Delete"]);
76
+
77
+ #inputEvent = new Event("input", { bubbles: true, cancelable: true });
78
+
79
+ /** Characters that will be automatically closed when typed. */
80
+ #keyPairs: Record<string, string> = {
81
+ "(": ")",
82
+ "{": "}",
83
+ "[": "]",
84
+ "<": ">",
85
+ '"': '"',
86
+ "`": "`",
87
+ };
88
+
89
+ constructor() {
90
+ super();
91
+
92
+ // add any `type: "wrap"` values from `contentElements` to `keyPairs`
93
+ for (const element of this.#contentElements) {
94
+ if (element.type === "wrap")
95
+ this.#keyPairs[element.value] = element.value;
96
+ }
97
+ }
98
+
99
+ /** The `content`, expects an `HTMLTextAreaElement`. */
100
+ get #textArea() {
101
+ return this.content(HTMLTextAreaElement);
102
+ }
103
+
104
+ /** The current `value` of the `textarea`. */
105
+ get #text() {
106
+ return this.#textArea.value;
107
+ }
108
+
109
+ set #text(value) {
110
+ this.#textArea.value = value;
111
+ this.#textArea.dispatchEvent(this.#inputEvent);
112
+ }
113
+
114
+ /** Array of `ContentElement`s derived from each `trigger`'s data attributes. */
115
+ get #contentElements() {
116
+ const contentElements: ContentElement[] = [];
117
+
118
+ for (const trigger of this.triggers()) {
119
+ contentElements.push(trigger.dataset as ContentElement);
120
+ }
121
+
122
+ return contentElements;
123
+ }
124
+
125
+ /** Gets the end position of the selection */
126
+ get #selEnd() {
127
+ return this.#textArea.selectionEnd;
128
+ }
129
+
130
+ /** Gets the start position of the selection. */
131
+ get #selStart() {
132
+ return this.#textArea.selectionStart;
133
+ }
134
+
135
+ /**
136
+ * @param str string to insert into `text`
137
+ * @param index where to insert the string
138
+ */
139
+ #insertStr(str: string, index: number) {
140
+ this.#text = this.#text.slice(0, index) + str + this.#text.slice(index);
141
+ }
142
+
143
+ /**
144
+ * @param start Starting index for removal.
145
+ * @param end Optional ending index - defaults to start + 1 to remove 1 character.
146
+ */
147
+ #removeStr(start: number, end = start + 1) {
148
+ this.#text = this.#text.slice(0, start) + this.#text.slice(end);
149
+ }
150
+
151
+ /** Sets the current cursor selection in the `textarea` */
152
+ #setSelection(start: number, end = start) {
153
+ this.#textArea.setSelectionRange(start, end);
154
+ this.#textArea.focus();
155
+ }
156
+
157
+ /**
158
+ * Inserts text and sets selection based on the `ContentElement` selected.
159
+ *
160
+ * @param content
161
+ */
162
+ #addContent({ value, type }: ContentElement) {
163
+ let start = this.#selStart;
164
+
165
+ if (type === "inline") {
166
+ // insert at current position
167
+ this.#insertStr(value, start);
168
+
169
+ const match = /[a-z]+/i.exec(value);
170
+
171
+ if (match?.index != null) {
172
+ start += match.index;
173
+ this.#setSelection(start, start + match[0].length);
174
+ } else {
175
+ this.#setSelection(start + value.length);
176
+ }
177
+ } else if (type === "wrap") {
178
+ const end = this.#selEnd + value.length;
179
+
180
+ this.#insertStr(value, start);
181
+ this.#insertStr(this.#keyPairs[value]!, end);
182
+ this.#setSelection(start + value.length, end);
183
+
184
+ // if single char, add to opened
185
+ if (value.length === 1) this.#openChars.push(value);
186
+ } else {
187
+ // "block"
188
+ const { lines, lineNumber } = this.#lineMeta();
189
+
190
+ // avoids `# # # `, instead adds trimmed => `### `
191
+ const firstChar = value[0];
192
+ if (firstChar && lines[lineNumber]?.startsWith(firstChar)) {
193
+ value = value.trim();
194
+ }
195
+
196
+ // add the string to the beginning of the line
197
+ lines[lineNumber] = value + lines[lineNumber];
198
+ this.#text = lines.join("\n");
199
+
200
+ this.#setSelection(start + value.length);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Checks if there is a block element at the beginning of the string.
206
+ *
207
+ * @param line
208
+ * @returns Whatever is found, otherwise null
209
+ */
210
+ #startsWithBlock(line: string) {
211
+ for (const blockString of this.#contentElements
212
+ .filter((el) => el.type === "block")
213
+ .map((el) => el.value)) {
214
+ if (line.startsWith(blockString)) return blockString;
215
+ }
216
+
217
+ return null;
218
+ }
219
+
220
+ /**
221
+ * @param line
222
+ * @returns The number, if the line starts with a number and a period.
223
+ */
224
+ #startsWithNumberAndPeriod(line: string) {
225
+ const match = line.match(/^(\d+)\./);
226
+ return match ? Number(match[1]) : null;
227
+ }
228
+
229
+ /**
230
+ * @returns Metadata describing the current position of the selection.
231
+ */
232
+ #lineMeta() {
233
+ const lines = this.#text.split("\n");
234
+ let charCount = 0;
235
+
236
+ for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
237
+ const line = lines[lineNumber]!;
238
+ const len = line.length + 1; // account for removed "\n" due to .split()
239
+ charCount += len;
240
+
241
+ // find the line that the cursor is on
242
+ if (charCount > this.#selEnd) {
243
+ return {
244
+ line,
245
+ lines,
246
+ lineNumber,
247
+ columnNumber: this.#selEnd - (charCount - len),
248
+ };
249
+ }
250
+ }
251
+
252
+ return { line: lines[0]!, lines, lineNumber: 0, columnNumber: 0 };
253
+ }
254
+
255
+ /**
256
+ * Increments/decrements the start of following lines if they are numbers.
257
+ *
258
+ * @param decrement if following lines should be decremented instead of incremented
259
+ *
260
+ * @example
261
+ *
262
+ * ```md
263
+ * Prevents this, instead fixes the following lines.
264
+ *
265
+ * 1. presses enter here when two items in list
266
+ * 2.
267
+ * 2. (repeat of 2)
268
+ * ```
269
+ */
270
+ #correctFollowing(decrement = false) {
271
+ let { lines, lineNumber } = this.#lineMeta();
272
+
273
+ for (; ++lineNumber < lines.length; ) {
274
+ let line = lines[lineNumber];
275
+
276
+ if (line) {
277
+ const num = this.#startsWithNumberAndPeriod(line);
278
+
279
+ if (num) {
280
+ let newNum: number;
281
+
282
+ if (decrement) {
283
+ if (num > 1) {
284
+ newNum = num - 1;
285
+ } else {
286
+ break;
287
+ }
288
+ } else {
289
+ newNum = num + 1;
290
+ }
291
+
292
+ lines[lineNumber] = newNum + line.slice(String(num).length);
293
+ } else {
294
+ break;
295
+ }
296
+ }
297
+ }
298
+
299
+ const start = this.#selStart;
300
+ this.#text = lines.join("\n");
301
+ this.#setSelection(start);
302
+ }
303
+
304
+ override mount() {
305
+ this.#textArea.addEventListener("keydown", (e) => {
306
+ const nextChar = this.#text[this.#selEnd] ?? "";
307
+ const notHighlighted = this.#selStart === this.#selEnd;
308
+
309
+ if (this.#resetKeys.has(e.key)) {
310
+ this.#openChars = [];
311
+ } else if (e.key === "Backspace") {
312
+ const prevChar = this.#text[this.#selStart - 1];
313
+
314
+ if (
315
+ prevChar &&
316
+ prevChar in this.#keyPairs &&
317
+ nextChar === this.#keyPairs[prevChar]
318
+ ) {
319
+ // remove both characters if the next one is the match of the prev
320
+ e.preventDefault();
321
+
322
+ const start = this.#selStart - 1;
323
+ const end = this.#selEnd - 1;
324
+
325
+ this.#removeStr(start);
326
+ this.#removeStr(end);
327
+ this.#setSelection(start, end);
328
+
329
+ this.#openChars.pop();
330
+ } else if (prevChar === "\n" && this.#selStart === this.#selEnd) {
331
+ e.preventDefault();
332
+
333
+ const newPos = this.#selStart - 1;
334
+
335
+ this.#correctFollowing(true);
336
+ this.#removeStr(newPos);
337
+ this.#setSelection(newPos, newPos);
338
+ }
339
+ } else if (e.key === "Tab") {
340
+ const blocks = this.#text.split("```");
341
+ let totalChars = 0;
342
+
343
+ for (const [i, block] of blocks.entries()) {
344
+ totalChars += block.length + 3;
345
+ if (totalChars > this.#selStart) {
346
+ if (i % 2) {
347
+ // caret is inside of a codeblock
348
+ e.preventDefault();
349
+
350
+ if (e.shiftKey) {
351
+ const { line, columnNumber } = this.#lineMeta();
352
+
353
+ if (line.startsWith("\t")) {
354
+ // dedent
355
+ const start = this.#selStart;
356
+ this.#removeStr(start - columnNumber);
357
+ this.#setSelection(start - 1);
358
+ }
359
+ } else {
360
+ // indent
361
+ this.#addContent({ type: "inline", value: "\t" });
362
+ }
363
+ }
364
+
365
+ break;
366
+ }
367
+ }
368
+ } else if (e.key === "Enter" && notHighlighted) {
369
+ // autocomplete start of next line if block or number
370
+ const { line, columnNumber } = this.#lineMeta();
371
+
372
+ let repeat = this.#startsWithBlock(line);
373
+ if (!repeat) {
374
+ const num = this.#startsWithNumberAndPeriod(line);
375
+ if (num) repeat = `${num + 1}. `;
376
+ }
377
+
378
+ if (repeat) {
379
+ e.preventDefault();
380
+
381
+ if (repeat.length < columnNumber) {
382
+ // repeat same on next line
383
+ this.#addContent({ type: "inline", value: "\n" + repeat });
384
+ this.#correctFollowing();
385
+ } else {
386
+ // remove repeat from current line
387
+ const end = this.#selEnd;
388
+ const newPos = end - repeat.length;
389
+
390
+ this.#removeStr(newPos, end);
391
+ this.#setSelection(newPos);
392
+ }
393
+ }
394
+ } else if ((e.ctrlKey || e.metaKey) && e.key) {
395
+ if (notHighlighted && (e.key === "c" || e.key === "x")) {
396
+ // copy or cut entire line
397
+ e.preventDefault();
398
+ const { line, lines, lineNumber, columnNumber } = this.#lineMeta();
399
+ navigator.clipboard.writeText(line);
400
+
401
+ if (e.key === "x") {
402
+ const newPos = this.#selStart - columnNumber;
403
+ lines.splice(lineNumber, 1);
404
+ this.#text = lines.join("\n");
405
+ this.#setSelection(newPos, newPos);
406
+ }
407
+ }
408
+
409
+ const shortcut = this.#contentElements.find((el) => el.key === e.key);
410
+ if (shortcut) this.#addContent(shortcut);
411
+ } else if (
412
+ this.#openChars.length &&
413
+ notHighlighted &&
414
+ (nextChar === e.key || e.key === "ArrowRight") &&
415
+ Object.values(this.#keyPairs).includes(nextChar)
416
+ ) {
417
+ // type over the next character instead of inserting
418
+ e.preventDefault();
419
+ this.#setSelection(this.#selStart + 1, this.#selEnd + 1);
420
+ this.#openChars.pop();
421
+ } else if (e.key in this.#keyPairs) {
422
+ e.preventDefault();
423
+ this.#addContent({ type: "wrap", value: e.key });
424
+ }
425
+ });
426
+
427
+ // trims the selection if there is an extra space around it
428
+ this.#textArea.addEventListener("dblclick", () => {
429
+ if (this.#selStart !== this.#selEnd) {
430
+ if (this.#text[this.#selStart] === " ") {
431
+ this.#setSelection(this.#selStart + 1, this.#selEnd);
432
+ }
433
+ if (this.#text[this.#selEnd - 1] === " ") {
434
+ this.#setSelection(this.#selStart, this.#selEnd - 1);
435
+ }
436
+ }
437
+ });
438
+
439
+ // reset #openChars on click since the cursor has changed position
440
+ this.#textArea.addEventListener("click", () => (this.#openChars = []));
441
+
442
+ this.listener((e) =>
443
+ this.#addContent(
444
+ (e.currentTarget as HTMLElement).dataset as ContentElement,
445
+ ),
446
+ );
447
+ }
448
+ }
@@ -0,0 +1,4 @@
1
+ import { define } from "../util/define.js";
2
+ import { Fullscreen } from "./index.js";
3
+
4
+ define("drab-fullscreen", Fullscreen);
@@ -0,0 +1,59 @@
1
+ import {
2
+ Content,
3
+ type ContentAttributes,
4
+ Lifecycle,
5
+ Trigger,
6
+ type TriggerAttributes,
7
+ } from "../base/index.js";
8
+
9
+ export interface FullscreenAttributes
10
+ extends TriggerAttributes,
11
+ ContentAttributes {}
12
+
13
+ /**
14
+ * Toggles the `documentElement` or `content` element to fullscreen mode.
15
+ *
16
+ * Disables the `trigger` if fullscreen is not supported.
17
+ */
18
+ export class Fullscreen extends Lifecycle(Trigger(Content())) {
19
+ constructor() {
20
+ super();
21
+ }
22
+
23
+ /**
24
+ * @returns `true` if fullscreen is currently enabled.
25
+ */
26
+ #isFullscreen() {
27
+ return document.fullscreenElement !== null;
28
+ }
29
+
30
+ /**
31
+ * @returns `true` if fullscreen is supported.
32
+ */
33
+ #fullscreenSupported() {
34
+ return "requestFullscreen" in document.documentElement;
35
+ }
36
+
37
+ /** Enables or disables fullscreen mode based on the current state. */
38
+ toggle() {
39
+ if (this.#isFullscreen()) {
40
+ document.exitFullscreen();
41
+ } else {
42
+ try {
43
+ this.content(HTMLElement).requestFullscreen();
44
+ } catch {
45
+ document.documentElement.requestFullscreen();
46
+ }
47
+ }
48
+ }
49
+
50
+ override mount() {
51
+ this.listener(() => this.toggle());
52
+
53
+ for (const trigger of this.triggers()) {
54
+ if (!this.#fullscreenSupported() && "disabled" in trigger) {
55
+ trigger.disabled = true;
56
+ }
57
+ }
58
+ }
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./announcer/index.js";
2
+ export * from "./contextmenu/index.js";
3
+ export * from "./dialog/index.js";
4
+ export * from "./editor/index.js";
5
+ export * from "./fullscreen/index.js";
6
+ export * from "./intersect/index.js";
7
+ export * from "./prefetch/index.js";
8
+ export * from "./share/index.js";
9
+ export * from "./tablesort/index.js";
10
+ export * from "./tabs/index.js";
11
+ export * from "./wakelock/index.js";
@@ -0,0 +1,4 @@
1
+ import { define } from "../util/define.js";
2
+ import { Intersect } from "./index.js";
3
+
4
+ define("drab-intersect", Intersect);
@@ -0,0 +1,79 @@
1
+ import {
2
+ Content,
3
+ type ContentAttributes,
4
+ Lifecycle,
5
+ Trigger,
6
+ type TriggerAttributes,
7
+ } from "../base/index.js";
8
+
9
+ export interface IntersectAttributes
10
+ extends TriggerAttributes,
11
+ ContentAttributes {
12
+ /** Number between 0 and 1 representing the visible portion of the `trigger`. */
13
+ threshold?: number;
14
+ }
15
+
16
+ /**
17
+ * Uses the
18
+ * [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
19
+ * to add a `data-intersect` attribute to `content` when the `trigger` is intersecting.
20
+ *
21
+ * ### Events
22
+ *
23
+ * `intersect`
24
+ *
25
+ * Fired when the `trigger` enters the viewport.
26
+ *
27
+ * `exit`
28
+ *
29
+ * Fired when the `trigger` exits the viewport.
30
+ *
31
+ * ### Attributes
32
+ *
33
+ * `threshold`
34
+ *
35
+ * Specify a `threshold` between `0` and `1` to determine how much of the
36
+ * `trigger` should be visible for the intersection to occur.
37
+ */
38
+ export class Intersect extends Lifecycle(Trigger(Content())) {
39
+ constructor() {
40
+ super();
41
+ }
42
+
43
+ /**
44
+ * How much of the `trigger` should be visible for the intersection to occur.
45
+ * For example, given a threshold of `.5`, the intersection would occur when
46
+ * the `trigger` is 50% visible.
47
+ *
48
+ * @default 0
49
+ */
50
+ get #threshold() {
51
+ return Number(this.getAttribute("threshold") ?? 0);
52
+ }
53
+
54
+ override mount() {
55
+ const observer = new IntersectionObserver(
56
+ (entries) => {
57
+ // attribute to add or remove from `content`
58
+ const attr = "data-intersect";
59
+
60
+ for (const entry of entries) {
61
+ if (entry.isIntersecting) {
62
+ this.content().setAttribute(attr, "");
63
+ } else {
64
+ this.content().removeAttribute(attr);
65
+ }
66
+
67
+ this.dispatchEvent(
68
+ new CustomEvent(entry.isIntersecting ? "intersect" : "exit", {
69
+ detail: { entry },
70
+ }),
71
+ );
72
+ }
73
+ },
74
+ { threshold: this.#threshold },
75
+ );
76
+
77
+ for (const trigger of this.triggers()) observer.observe(trigger);
78
+ }
79
+ }
@@ -0,0 +1,4 @@
1
+ import { define } from "../util/define.js";
2
+ import { Prefetch } from "./index.js";
3
+
4
+ define("drab-prefetch", Prefetch);