drab 7.0.1 → 7.0.3
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 +2 -2
- 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 +178 -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,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,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,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
|
+
}
|