autumnnote 2.0.0 → 2.2.0
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/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2866
package/src/js/editing/Style.js
DELETED
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Style.js - Inline / block style detection and application utilities
|
|
3
|
-
* Rewritten from Summernote's approach using vanilla JS + execCommand fallback
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { closest, isElement, isPara } from '../core/dom.js';
|
|
7
|
-
import { currentRange } from '../core/range.js';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// execCommand wrappers (still the most compatible way in contenteditable)
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Applies a document execCommand.
|
|
15
|
-
* @param {string} cmd
|
|
16
|
-
* @param {string} [value]
|
|
17
|
-
* @returns {boolean}
|
|
18
|
-
*/
|
|
19
|
-
export function execCommand(cmd, value = null) {
|
|
20
|
-
return document.execCommand(cmd, false, value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Inline style helpers
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Bolds / unbolds the selection.
|
|
29
|
-
*/
|
|
30
|
-
export const bold = () => execCommand('bold');
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Italicises / un-italicises the selection.
|
|
34
|
-
*/
|
|
35
|
-
export const italic = () => execCommand('italic');
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Underlines / un-underlines the selection.
|
|
39
|
-
* Falls back to manual DOM manipulation when inside <code> where
|
|
40
|
-
* execCommand's state detection is unreliable.
|
|
41
|
-
*/
|
|
42
|
-
export function underline() {
|
|
43
|
-
const sel = globalThis.getSelection();
|
|
44
|
-
if (!sel?.rangeCount) return;
|
|
45
|
-
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
46
|
-
if (container.nodeType === 3) container = container.parentElement;
|
|
47
|
-
// Check if we're inside a <u> (DOM truth), to guard against unreliable queryCommandState
|
|
48
|
-
const uEl = /** @type {Element|null} */ (container)?.closest('u');
|
|
49
|
-
const nativeState = document.queryCommandState('underline');
|
|
50
|
-
if (uEl && !nativeState) {
|
|
51
|
-
// Browser doesn't recognise the underline state (e.g. inside <code>).
|
|
52
|
-
// Manually unwrap the <u> element.
|
|
53
|
-
const parent = uEl.parentNode;
|
|
54
|
-
while (uEl.firstChild) parent.insertBefore(uEl.firstChild, uEl);
|
|
55
|
-
uEl.remove();
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
execCommand('underline');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Strikethrough / removes strikethrough.
|
|
63
|
-
* Falls back to manual DOM manipulation inside nested formats where
|
|
64
|
-
* execCommand's state detection is unreliable (mirrors underline() logic).
|
|
65
|
-
*/
|
|
66
|
-
export function strikethrough() {
|
|
67
|
-
const sel = globalThis.getSelection();
|
|
68
|
-
if (!sel?.rangeCount) return;
|
|
69
|
-
// Use startContainer for consistent detection across collapsed and range
|
|
70
|
-
// selections — commonAncestorContainer can miss ancestor <s>/<strike> tags
|
|
71
|
-
// when the selection spans across nested inline elements.
|
|
72
|
-
let sc = sel.getRangeAt(0).startContainer;
|
|
73
|
-
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
74
|
-
const sEl = /** @type {Element|null} */ (sc)?.closest('s') || /** @type {Element|null} */ (sc)?.closest('strike');
|
|
75
|
-
const nativeState = document.queryCommandState('strikeThrough');
|
|
76
|
-
if (sEl && !nativeState) {
|
|
77
|
-
// Browser doesn’t recognise the strikethrough state (e.g. inside <code>
|
|
78
|
-
// or deeply nested inline formats). Manually unwrap the <s>/<strike>.
|
|
79
|
-
const parent = sEl.parentNode;
|
|
80
|
-
while (sEl.firstChild) parent.insertBefore(sEl.firstChild, sEl);
|
|
81
|
-
sEl.remove();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
execCommand('strikeThrough');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Superscript toggle.
|
|
89
|
-
*/
|
|
90
|
-
export const superscript = () => execCommand('superscript');
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Subscript toggle.
|
|
94
|
-
*/
|
|
95
|
-
export const subscript = () => execCommand('subscript');
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Sets the foreground colour of the selected text.
|
|
99
|
-
* @param {string} color - CSS colour string
|
|
100
|
-
*/
|
|
101
|
-
export const foreColor = (color) => execCommand('foreColor', color);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Sets the background (highlight) colour of the selected text.
|
|
105
|
-
* @param {string} color - CSS colour string
|
|
106
|
-
*/
|
|
107
|
-
export const backColor = (color) => execCommand('hiliteColor', color);
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Sets the font name for the selection.
|
|
111
|
-
* @param {string} name
|
|
112
|
-
*/
|
|
113
|
-
export const fontName = (name) => execCommand('fontName', name);
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Sets the font size (in pt or with unit) for the selection.
|
|
117
|
-
* Uses a span-based approach to set px sizes precisely.
|
|
118
|
-
* @param {string} size - e.g. '14px'
|
|
119
|
-
* @param {HTMLElement|Document} [editable] - scoping element to avoid touching nodes outside this editor
|
|
120
|
-
*/
|
|
121
|
-
export function fontSize(size, editable = document) {
|
|
122
|
-
const sel = globalThis.getSelection();
|
|
123
|
-
const wasCollapsed = !sel?.rangeCount || sel.getRangeAt(0).collapsed;
|
|
124
|
-
|
|
125
|
-
// B-I-3/4: For a collapsed (caret) selection the browser's execCommand
|
|
126
|
-
// 'fontSize' leaves an internal "pending" state of size-7 (=48px) instead of
|
|
127
|
-
// creating a <font> element, so the very next typed character comes out at
|
|
128
|
-
// 48px. Fix: bypass execCommand entirely for collapsed selections and directly
|
|
129
|
-
// insert a span with the requested size, placing the cursor inside it.
|
|
130
|
-
// Only applies when there IS an active selection (sel.rangeCount > 0); when
|
|
131
|
-
// there is no selection at all (e.g. jsdom unit tests) fall through to the
|
|
132
|
-
// execCommand path so the font-replacement logic still runs.
|
|
133
|
-
if (wasCollapsed && sel?.rangeCount > 0) {
|
|
134
|
-
try {
|
|
135
|
-
const range = sel.getRangeAt(0);
|
|
136
|
-
const span = document.createElement('span');
|
|
137
|
-
span.style.fontSize = size;
|
|
138
|
-
const zwsNode = document.createTextNode('\u200B');
|
|
139
|
-
span.appendChild(zwsNode);
|
|
140
|
-
range.insertNode(span);
|
|
141
|
-
const nr = document.createRange();
|
|
142
|
-
nr.setStart(zwsNode, zwsNode.textContent.length);
|
|
143
|
-
nr.collapse(true);
|
|
144
|
-
sel.removeAllRanges();
|
|
145
|
-
sel.addRange(nr);
|
|
146
|
-
} catch (_) { void _; /* ignore range errors on unusual DOM structures */ }
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Non-collapsed selection (or no selection — handles jsdom test setup where
|
|
151
|
-
// <font size="7"> elements are injected directly without a live selection):
|
|
152
|
-
// use execCommand placeholder approach then replace <font> with <span>.
|
|
153
|
-
execCommand('fontSize', '7');
|
|
154
|
-
const scope = editable instanceof HTMLElement ? editable : document;
|
|
155
|
-
const newSpans = [];
|
|
156
|
-
scope.querySelectorAll('font[size="7"]').forEach((el) => {
|
|
157
|
-
const span = document.createElement('span');
|
|
158
|
-
span.style.fontSize = size;
|
|
159
|
-
el.parentNode.insertBefore(span, el);
|
|
160
|
-
while (el.firstChild) span.appendChild(el.firstChild);
|
|
161
|
-
el.remove();
|
|
162
|
-
newSpans.push(span);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Re-select all replaced content so toolbar getValue() reads the new size
|
|
166
|
-
// (B-I-1/2: without this re-selection the toolbar dropdown stays on the old
|
|
167
|
-
// value until the next selectionchange event).
|
|
168
|
-
if (!wasCollapsed && sel && newSpans.length > 0) {
|
|
169
|
-
const first = newSpans[0];
|
|
170
|
-
const last = newSpans.at(-1);
|
|
171
|
-
try {
|
|
172
|
-
const nr = document.createRange();
|
|
173
|
-
const startNode = first.firstChild || first;
|
|
174
|
-
const endNode = last.lastChild || last;
|
|
175
|
-
nr.setStart(startNode, 0);
|
|
176
|
-
nr.setEnd(endNode, endNode.nodeType === Node.TEXT_NODE ? endNode.textContent.length : endNode.childNodes.length);
|
|
177
|
-
sel.removeAllRanges();
|
|
178
|
-
sel.addRange(nr);
|
|
179
|
-
} catch (_) { void _; /* ignore range errors on unusual DOM structures */ }
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// Block style helpers
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Wraps the selection in the given block tag (p, h1-h6, blockquote, pre).
|
|
189
|
-
* @param {string} tagName
|
|
190
|
-
*/
|
|
191
|
-
export const formatBlock = (tagName) => execCommand('formatBlock', `<${tagName}>`);
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Left-aligns the current block.
|
|
195
|
-
*/
|
|
196
|
-
export const justifyLeft = () => execCommand('justifyLeft');
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Center-aligns the current block.
|
|
200
|
-
*/
|
|
201
|
-
export const justifyCenter = () => execCommand('justifyCenter');
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Right-aligns the current block.
|
|
205
|
-
*/
|
|
206
|
-
export const justifyRight = () => execCommand('justifyRight');
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Fully justifies the current block.
|
|
210
|
-
*/
|
|
211
|
-
export const justifyFull = () => execCommand('justifyFull');
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Indents the list or block.
|
|
215
|
-
*/
|
|
216
|
-
export const indent = () => execCommand('indent');
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Outdents the list or block.
|
|
220
|
-
* G.5: When cursor is inside a checklist item, "outdent" means converting
|
|
221
|
-
* that item back to a regular <p> element rather than calling execCommand
|
|
222
|
-
* (which would destroy the ul > li checklist structure).
|
|
223
|
-
*/
|
|
224
|
-
export function outdent() {
|
|
225
|
-
const sel = globalThis.getSelection();
|
|
226
|
-
if (sel?.rangeCount) {
|
|
227
|
-
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
228
|
-
if (container.nodeType === 3) container = container.parentElement;
|
|
229
|
-
const checkLi = /** @type {Element|null} */ (container)?.closest('.an-checklist li');
|
|
230
|
-
if (checkLi) {
|
|
231
|
-
_checklistItemToP(/** @type {HTMLElement} */ (checkLi));
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
execCommand('outdent');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Convert a checklist <li> into a paragraph and move any following items into a new checklist.
|
|
240
|
-
*
|
|
241
|
-
* Preserves inline markup from the converted item, strips zero-width space anchors,
|
|
242
|
-
* and replaces empty content with a non‑breaking space. If there are list items
|
|
243
|
-
* after the converted item they are moved into a new <ul class="an-checklist">
|
|
244
|
-
* inserted immediately after the original list. The original <li> is removed and
|
|
245
|
-
* the original list is removed if it becomes empty. Attempts to place the caret
|
|
246
|
-
* at the start of the newly created <p>.
|
|
247
|
-
* @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
|
|
248
|
-
*/
|
|
249
|
-
function _checklistItemToP(checkLi) {
|
|
250
|
-
const checkUl = checkLi.closest('.an-checklist');
|
|
251
|
-
if (!checkUl) return;
|
|
252
|
-
|
|
253
|
-
const allLis = Array.from(checkUl.children);
|
|
254
|
-
const liIndex = allLis.indexOf(checkLi);
|
|
255
|
-
const afterLis = allLis.slice(liIndex + 1);
|
|
256
|
-
|
|
257
|
-
// Build <p> preserving inline formatting (bold/italic/links) from the item's content
|
|
258
|
-
const p = document.createElement('p');
|
|
259
|
-
for (const child of checkLi.childNodes) {
|
|
260
|
-
if (child.nodeType === 1 && /** @type {Element} */ (child).tagName === 'INPUT') continue;
|
|
261
|
-
p.appendChild(child.cloneNode(true));
|
|
262
|
-
}
|
|
263
|
-
// Strip ZWS anchors left over from checklist markup
|
|
264
|
-
p.innerHTML = p.innerHTML.replaceAll('\u200B', '');
|
|
265
|
-
if (!p.hasChildNodes() || !p.textContent.trim()) {
|
|
266
|
-
p.innerHTML = '';
|
|
267
|
-
p.appendChild(document.createTextNode('\u00a0'));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Move items after the current li into a new checklist
|
|
271
|
-
if (afterLis.length > 0) {
|
|
272
|
-
const newUl = document.createElement('ul');
|
|
273
|
-
newUl.className = 'an-checklist';
|
|
274
|
-
afterLis.forEach(li => newUl.appendChild(li));
|
|
275
|
-
checkUl.parentNode.insertBefore(newUl, checkUl.nextSibling);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Insert <p> after checkUl (before any newUl)
|
|
279
|
-
checkUl.parentNode.insertBefore(p, checkUl.nextSibling);
|
|
280
|
-
|
|
281
|
-
// Remove current li from checkUl; delete checkUl if now empty
|
|
282
|
-
checkLi.remove();
|
|
283
|
-
if (checkUl.children.length === 0) checkUl.remove();
|
|
284
|
-
|
|
285
|
-
// Place caret at start of the new <p>
|
|
286
|
-
try {
|
|
287
|
-
const nr = document.createRange();
|
|
288
|
-
const firstChild = p.firstChild;
|
|
289
|
-
nr.setStart(firstChild?.nodeType === 3 ? firstChild : p, 0);
|
|
290
|
-
nr.collapse(true);
|
|
291
|
-
const s = globalThis.getSelection();
|
|
292
|
-
if (s) { s.removeAllRanges(); s.addRange(nr); }
|
|
293
|
-
} catch {}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Inserts an unordered (bulleted) list, or converts the current list to `<ul>`.
|
|
298
|
-
*
|
|
299
|
-
* When the cursor is already inside a list, direct DOM manipulation is used to
|
|
300
|
-
* transition between list types — `execCommand` alone cannot handle checklist →
|
|
301
|
-
* UL/OL conversions because it has no awareness of the `an-checklist` class or
|
|
302
|
-
* the checkbox `<input>` elements.
|
|
303
|
-
*
|
|
304
|
-
* Transition paths:
|
|
305
|
-
* - **Checklist → UL**: strips `an-checklist` class and all checkbox inputs;
|
|
306
|
-
* converts `<ol>` container to `<ul>` via `changeTagName()` if needed.
|
|
307
|
-
* - **OL → UL**: swaps the container tag via `changeTagName()`.
|
|
308
|
-
* - **UL → paragraphs**: falls back to `execCommand('insertUnorderedList')`
|
|
309
|
-
* which toggles the list off (browser-native behaviour).
|
|
310
|
-
* - **No list → UL**: falls back to `execCommand('insertUnorderedList')`.
|
|
311
|
-
*/
|
|
312
|
-
/**
|
|
313
|
-
* Helper to get the closest ul/ol element containing the current selection.
|
|
314
|
-
* @returns {Element|null}
|
|
315
|
-
*/
|
|
316
|
-
function getSelectedList() {
|
|
317
|
-
const sel = globalThis.getSelection();
|
|
318
|
-
if (!sel?.rangeCount) return null;
|
|
319
|
-
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
320
|
-
if (container.nodeType === 3) container = container.parentElement;
|
|
321
|
-
return /** @type {Element|null} */ (container)?.closest('ul, ol') || null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Strips the checklist class and checkbox inputs from a list element.
|
|
326
|
-
* @param {Element} listEl
|
|
327
|
-
*/
|
|
328
|
-
function stripChecklist(listEl) {
|
|
329
|
-
listEl.classList.remove('an-checklist');
|
|
330
|
-
listEl.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.remove());
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function insertUnorderedList() {
|
|
334
|
-
const listEl = getSelectedList();
|
|
335
|
-
if (listEl) {
|
|
336
|
-
if (listEl.classList.contains('an-checklist')) {
|
|
337
|
-
// Checklist → UL: strip checkboxes and class, swap tag if needed
|
|
338
|
-
stripChecklist(listEl);
|
|
339
|
-
if (listEl.tagName === 'OL') {
|
|
340
|
-
changeTagName(listEl, 'ul');
|
|
341
|
-
}
|
|
342
|
-
} else if (listEl.tagName === 'OL') {
|
|
343
|
-
// OL → UL: swap container tag
|
|
344
|
-
changeTagName(listEl, 'ul');
|
|
345
|
-
} else {
|
|
346
|
-
// Already UL → toggle off via execCommand
|
|
347
|
-
execCommand('insertUnorderedList');
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
// Not in a list → create new UL via execCommand
|
|
351
|
-
execCommand('insertUnorderedList');
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Inserts an ordered (numbered) list, or converts the current list to `<ol>`.
|
|
357
|
-
*
|
|
358
|
-
* When the cursor is already inside a list, direct DOM manipulation is used to
|
|
359
|
-
* transition between list types — `execCommand` alone cannot handle checklist →
|
|
360
|
-
* UL/OL conversions because it has no awareness of the `an-checklist` class or
|
|
361
|
-
* the checkbox `<input>` elements.
|
|
362
|
-
*
|
|
363
|
-
* Transition paths:
|
|
364
|
-
* - **Checklist → OL**: strips `an-checklist` class and all checkbox inputs;
|
|
365
|
-
* converts container to `<ol>` via `changeTagName()`.
|
|
366
|
-
* - **UL → OL**: swaps the container tag via `changeTagName()`.
|
|
367
|
-
* - **OL → paragraphs**: falls back to `execCommand('insertOrderedList')`
|
|
368
|
-
* which toggles the list off (browser-native behaviour).
|
|
369
|
-
* - **No list → OL**: falls back to `execCommand('insertOrderedList')`.
|
|
370
|
-
*/
|
|
371
|
-
export function insertOrderedList() {
|
|
372
|
-
const listEl = getSelectedList();
|
|
373
|
-
if (listEl) {
|
|
374
|
-
if (listEl.classList.contains('an-checklist')) {
|
|
375
|
-
// Checklist → OL: strip checkboxes and class, swap to <ol>
|
|
376
|
-
stripChecklist(listEl);
|
|
377
|
-
changeTagName(listEl, 'ol');
|
|
378
|
-
} else if (listEl.tagName === 'UL') {
|
|
379
|
-
// UL → OL: swap container tag
|
|
380
|
-
changeTagName(listEl, 'ol');
|
|
381
|
-
} else {
|
|
382
|
-
// Already OL → toggle off via execCommand
|
|
383
|
-
execCommand('insertOrderedList');
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
// Not in a list → create new OL via execCommand
|
|
387
|
-
execCommand('insertOrderedList');
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ---------------------------------------------------------------------------
|
|
392
|
-
// Line-height helper
|
|
393
|
-
// ---------------------------------------------------------------------------
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Set the line-height on every block-level element that intersects the current selection.
|
|
397
|
-
*
|
|
398
|
-
* If the selection is collapsed, the nearest enclosing block element receives the style.
|
|
399
|
-
* For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
|
|
400
|
-
* if none are found, the nearest block ancestor of the range's common ancestor is updated.
|
|
401
|
-
* @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
|
|
402
|
-
*/
|
|
403
|
-
export function lineHeight(value) {
|
|
404
|
-
const sel = globalThis.getSelection();
|
|
405
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
406
|
-
|
|
407
|
-
const range = sel.getRangeAt(0);
|
|
408
|
-
const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE', 'PRE', 'TD', 'TH']);
|
|
409
|
-
|
|
410
|
-
const nearestBlock = (node) => {
|
|
411
|
-
let el = node instanceof Element ? node : node.parentElement;
|
|
412
|
-
while (el) {
|
|
413
|
-
if (BLOCK_TAGS.has(el.tagName)) return el;
|
|
414
|
-
el = el.parentElement;
|
|
415
|
-
}
|
|
416
|
-
return null;
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
if (range.collapsed) {
|
|
420
|
-
const block = nearestBlock(range.startContainer);
|
|
421
|
-
if (block) block.style.lineHeight = value;
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// For a range selection, collect all unique block ancestors of text nodes
|
|
426
|
-
const blocks = new Set();
|
|
427
|
-
const iter = document.createTreeWalker(
|
|
428
|
-
range.commonAncestorContainer,
|
|
429
|
-
NodeFilter.SHOW_TEXT,
|
|
430
|
-
{ acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP },
|
|
431
|
-
);
|
|
432
|
-
let textNode;
|
|
433
|
-
while ((textNode = iter.nextNode())) {
|
|
434
|
-
const block = nearestBlock(textNode);
|
|
435
|
-
if (block) blocks.add(block);
|
|
436
|
-
}
|
|
437
|
-
if (blocks.size === 0) {
|
|
438
|
-
const block = nearestBlock(range.commonAncestorContainer);
|
|
439
|
-
if (block) blocks.add(block);
|
|
440
|
-
}
|
|
441
|
-
blocks.forEach((block) => { block.style.lineHeight = value; });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
// Style query helpers
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Returns the computed styles relevant to the current cursor position.
|
|
450
|
-
* @param {HTMLElement} editable
|
|
451
|
-
* @returns {object} styleMap
|
|
452
|
-
*/
|
|
453
|
-
export function currentStyle(editable) {
|
|
454
|
-
const range = currentRange(editable);
|
|
455
|
-
if (!range) return {};
|
|
456
|
-
|
|
457
|
-
const container = range.isCollapsed()
|
|
458
|
-
? range.sc
|
|
459
|
-
: range.commonAncestor();
|
|
460
|
-
|
|
461
|
-
const el = /** @type {Element|null} */ (isElement(container) ? container : container.parentElement);
|
|
462
|
-
if (!el) return {};
|
|
463
|
-
|
|
464
|
-
const computed = globalThis.getComputedStyle(el);
|
|
465
|
-
|
|
466
|
-
return {
|
|
467
|
-
bold: document.queryCommandState('bold'),
|
|
468
|
-
italic: document.queryCommandState('italic'),
|
|
469
|
-
underline: document.queryCommandState('underline'),
|
|
470
|
-
strikethrough: document.queryCommandState('strikeThrough'),
|
|
471
|
-
superscript: document.queryCommandState('superscript'),
|
|
472
|
-
subscript: document.queryCommandState('subscript'),
|
|
473
|
-
fontSize: computed.fontSize,
|
|
474
|
-
fontFamily: computed.fontFamily,
|
|
475
|
-
color: computed.color,
|
|
476
|
-
backgroundColor: computed.backgroundColor,
|
|
477
|
-
textAlign: computed.textAlign,
|
|
478
|
-
lineHeight: computed.lineHeight,
|
|
479
|
-
formatBlock: (closest(el, isPara, editable) || { nodeName: 'p' }).nodeName.toLowerCase(),
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ---------------------------------------------------------------------------
|
|
484
|
-
// Inline code toggle
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Wraps the selection in an inline <code> element, or unwraps it if the
|
|
489
|
-
* cursor is already inside a <code> that is not inside a <pre>.
|
|
490
|
-
* @param {HTMLElement} [_editable]
|
|
491
|
-
*/
|
|
492
|
-
export function toggleInlineCode(_editable) {
|
|
493
|
-
const sel = globalThis.getSelection();
|
|
494
|
-
if (!sel?.rangeCount) return;
|
|
495
|
-
const range = sel.getRangeAt(0);
|
|
496
|
-
let container = range.commonAncestorContainer;
|
|
497
|
-
if (container.nodeType === 3) container = container.parentElement;
|
|
498
|
-
const codeEl = /** @type {Element|null} */ (container)?.closest('code');
|
|
499
|
-
if (codeEl && !codeEl.closest('pre')) {
|
|
500
|
-
// Unwrap — save range endpoints relative to surrounding text so we can
|
|
501
|
-
// restore the selection after normalize() merges adjacent text nodes.
|
|
502
|
-
const parent = codeEl.parentNode;
|
|
503
|
-
// Note the sibling before the code element so we can re-anchor later.
|
|
504
|
-
const prevSibling = codeEl.previousSibling;
|
|
505
|
-
const movedChildren = Array.from(codeEl.childNodes);
|
|
506
|
-
while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
|
|
507
|
-
codeEl.remove();
|
|
508
|
-
// Normalize only the immediate parent to merge adjacent text nodes without
|
|
509
|
-
// invalidating distant selection anchors (full editable.normalize() can
|
|
510
|
-
// cause selection offsets to shift, making subsequent format toggles miss).
|
|
511
|
-
parent?.normalize();
|
|
512
|
-
// Restore selection to the text that was inside the unwrapped <code>.
|
|
513
|
-
if (movedChildren.length > 0) {
|
|
514
|
-
try {
|
|
515
|
-
// After normalize, find the merged text node that contains the content.
|
|
516
|
-
const firstMoved = movedChildren[0];
|
|
517
|
-
const lastMoved = movedChildren.at(-1);
|
|
518
|
-
const nr = document.createRange();
|
|
519
|
-
// Use the (possibly merged) live node if still in the DOM.
|
|
520
|
-
const anchorNode = firstMoved.parentNode === parent
|
|
521
|
-
? firstMoved
|
|
522
|
-
: (prevSibling ? prevSibling.nextSibling : parent.firstChild);
|
|
523
|
-
if (anchorNode) {
|
|
524
|
-
nr.setStart(anchorNode, 0);
|
|
525
|
-
const endAnchor = (lastMoved.parentNode === parent) ? lastMoved : anchorNode;
|
|
526
|
-
nr.setEnd(endAnchor, endAnchor.nodeType === Node.TEXT_NODE ? endAnchor.textContent.length : endAnchor.childNodes.length);
|
|
527
|
-
sel.removeAllRanges();
|
|
528
|
-
sel.addRange(nr);
|
|
529
|
-
}
|
|
530
|
-
} catch (_) { void _; /* ignore */ }
|
|
531
|
-
}
|
|
532
|
-
} else {
|
|
533
|
-
if (range.collapsed) return;
|
|
534
|
-
try {
|
|
535
|
-
const code = document.createElement('code');
|
|
536
|
-
range.surroundContents(code);
|
|
537
|
-
// Re-select wrapped content so subsequent format toggles work
|
|
538
|
-
const newRange = document.createRange();
|
|
539
|
-
newRange.selectNodeContents(code);
|
|
540
|
-
sel.removeAllRanges();
|
|
541
|
-
sel.addRange(newRange);
|
|
542
|
-
} catch {
|
|
543
|
-
// surroundContents fails across element boundaries — extract and rewrap
|
|
544
|
-
const frag = range.extractContents();
|
|
545
|
-
const code = document.createElement('code');
|
|
546
|
-
code.appendChild(frag);
|
|
547
|
-
range.insertNode(code);
|
|
548
|
-
// Re-select wrapped content
|
|
549
|
-
const newRange = document.createRange();
|
|
550
|
-
newRange.selectNodeContents(code);
|
|
551
|
-
sel.removeAllRanges();
|
|
552
|
-
sel.addRange(newRange);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Returns true when the cursor / selection is inside an inline <code>
|
|
559
|
-
* (not nested in a <pre>).
|
|
560
|
-
* Uses startContainer for reliable cross-browser detection regardless of
|
|
561
|
-
* whether the selection is collapsed or a range (commonAncestorContainer
|
|
562
|
-
* can behave inconsistently for range selections on some browsers).
|
|
563
|
-
* @returns {boolean}
|
|
564
|
-
*/
|
|
565
|
-
export function isInlineCode() {
|
|
566
|
-
const sel = globalThis.getSelection();
|
|
567
|
-
if (!sel?.rangeCount) return false;
|
|
568
|
-
let sc = sel.getRangeAt(0).startContainer;
|
|
569
|
-
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
570
|
-
const code = /** @type {Element|null} */ (sc)?.closest('code');
|
|
571
|
-
return !!(code && !code.closest('pre'));
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// ---------------------------------------------------------------------------
|
|
575
|
-
// Checklist (task list)
|
|
576
|
-
// ---------------------------------------------------------------------------
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Changes the tag name of an element in the DOM while preserving attributes and children.
|
|
580
|
-
* @param {Element} el
|
|
581
|
-
* @param {string} newTagName
|
|
582
|
-
* @returns {HTMLElement}
|
|
583
|
-
*/
|
|
584
|
-
function changeTagName(el, newTagName) {
|
|
585
|
-
const newEl = document.createElement(newTagName);
|
|
586
|
-
for (const attr of el.attributes) {
|
|
587
|
-
newEl.setAttribute(attr.name, attr.value);
|
|
588
|
-
}
|
|
589
|
-
while (el.firstChild) {
|
|
590
|
-
newEl.appendChild(el.firstChild);
|
|
591
|
-
}
|
|
592
|
-
el.parentNode.replaceChild(newEl, el);
|
|
593
|
-
return newEl;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Ensures all list items under the list element have a checkbox.
|
|
598
|
-
* @param {Element} listEl
|
|
599
|
-
*/
|
|
600
|
-
function ensureCheckboxes(listEl) {
|
|
601
|
-
listEl.querySelectorAll('li').forEach(li => {
|
|
602
|
-
const existingCb = li.querySelector('input[type="checkbox"]');
|
|
603
|
-
if (!existingCb) {
|
|
604
|
-
const cb = document.createElement('input');
|
|
605
|
-
cb.type = 'checkbox';
|
|
606
|
-
cb.contentEditable = 'false';
|
|
607
|
-
li.insertBefore(cb, li.firstChild);
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Toggle a checklist at the current selection or caret.
|
|
614
|
-
*/
|
|
615
|
-
export function toggleChecklist() {
|
|
616
|
-
const sel = globalThis.getSelection();
|
|
617
|
-
if (!sel?.rangeCount) return;
|
|
618
|
-
const range = sel.getRangeAt(0);
|
|
619
|
-
let container = range.commonAncestorContainer;
|
|
620
|
-
if (container.nodeType === 3) container = container.parentElement;
|
|
621
|
-
|
|
622
|
-
const listEl = /** @type {Element|null} */ (container)?.closest('ul, ol');
|
|
623
|
-
if (listEl) {
|
|
624
|
-
if (listEl.classList.contains('an-checklist')) {
|
|
625
|
-
// Transition from Checklist to Paragraphs (Toggle off checklist entirely)
|
|
626
|
-
const parent = listEl.parentNode;
|
|
627
|
-
if (parent) {
|
|
628
|
-
const lis = Array.from(listEl.children);
|
|
629
|
-
let /** @type {HTMLParagraphElement|null} */ firstP = null;
|
|
630
|
-
lis.forEach(li => {
|
|
631
|
-
const p = document.createElement('p');
|
|
632
|
-
for (const child of li.childNodes) {
|
|
633
|
-
if (child.nodeType === 1 && /** @type {Element} */ (child).tagName === 'INPUT') continue;
|
|
634
|
-
p.appendChild(child.cloneNode(true));
|
|
635
|
-
}
|
|
636
|
-
p.innerHTML = p.innerHTML.replaceAll('\u200b', '').replaceAll('\u200B', '');
|
|
637
|
-
if (!p.hasChildNodes() || !p.textContent.trim()) {
|
|
638
|
-
p.innerHTML = '';
|
|
639
|
-
p.appendChild(document.createTextNode('\u00a0'));
|
|
640
|
-
}
|
|
641
|
-
listEl.before(p);
|
|
642
|
-
if (!firstP) firstP = p;
|
|
643
|
-
});
|
|
644
|
-
listEl.remove();
|
|
645
|
-
|
|
646
|
-
if (firstP) {
|
|
647
|
-
const nr = document.createRange();
|
|
648
|
-
nr.setStart(firstP.firstChild || firstP, 0);
|
|
649
|
-
nr.collapse(true);
|
|
650
|
-
sel.removeAllRanges();
|
|
651
|
-
sel.addRange(nr);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
} else {
|
|
655
|
-
// Transition from standard UL/OL to Checklist
|
|
656
|
-
const targetUl = changeTagName(listEl, 'ul');
|
|
657
|
-
targetUl.classList.add('an-checklist');
|
|
658
|
-
ensureCheckboxes(targetUl);
|
|
659
|
-
|
|
660
|
-
// Place caret inside the first LI
|
|
661
|
-
const firstLi = targetUl.querySelector('li');
|
|
662
|
-
if (firstLi) {
|
|
663
|
-
const nr = document.createRange();
|
|
664
|
-
nr.selectNodeContents(firstLi);
|
|
665
|
-
nr.collapse(false);
|
|
666
|
-
sel.removeAllRanges();
|
|
667
|
-
sel.addRange(nr);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
} else {
|
|
671
|
-
// Selection is not in a list: build the checklist directly via DOM
|
|
672
|
-
// manipulation. execCommand('insertUnorderedList') is intentionally
|
|
673
|
-
// avoided here — its behaviour on collapsed/empty selections and
|
|
674
|
-
// non-standard blocks (e.g. <section>) is too inconsistent across
|
|
675
|
-
// browsers (and a no-op in jsdom), which left toggleChecklist() as a
|
|
676
|
-
// silent no-op in those cases.
|
|
677
|
-
// The editable root itself is a <div> and must never be treated as a
|
|
678
|
-
// "block" to convert/replace/remove — otherwise selections that include
|
|
679
|
-
// raw text nodes sitting directly inside it (e.g. the first line typed
|
|
680
|
-
// into an empty editor) would destroy the .an-editable element.
|
|
681
|
-
const editableRoot = /** @type {Element|null} */ (container)?.closest('[contenteditable="true"]');
|
|
682
|
-
const isCollapsed = range.collapsed;
|
|
683
|
-
if (isCollapsed) {
|
|
684
|
-
// Find the nearest block-level ancestor (p, div, li, h1-h6, blockquote,
|
|
685
|
-
// etc.) and convert it into a single checklist item.
|
|
686
|
-
const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI']);
|
|
687
|
-
let block = /** @type {Element|null} */ (container);
|
|
688
|
-
while (block?.parentNode && block !== editableRoot && !BLOCK_TAGS.has(block.tagName)) {
|
|
689
|
-
block = /** @type {Element|null} */ (block.parentNode);
|
|
690
|
-
}
|
|
691
|
-
if (block === editableRoot) block = null;
|
|
692
|
-
// Fallback: if no block element found (e.g. cursor directly in editable
|
|
693
|
-
// root), insert a fresh item with a zero-width-space so the cursor ends
|
|
694
|
-
// up inside it.
|
|
695
|
-
const itemText = (block && BLOCK_TAGS.has(block.tagName))
|
|
696
|
-
? Array.from(block.childNodes)
|
|
697
|
-
.map((n) => n.textContent)
|
|
698
|
-
.join('')
|
|
699
|
-
.replaceAll('\u00a0', ' ')
|
|
700
|
-
: '';
|
|
701
|
-
|
|
702
|
-
const newUl = document.createElement('ul');
|
|
703
|
-
newUl.className = 'an-checklist';
|
|
704
|
-
const li = document.createElement('li');
|
|
705
|
-
const checkbox = document.createElement('input');
|
|
706
|
-
checkbox.type = 'checkbox';
|
|
707
|
-
checkbox.contentEditable = 'false';
|
|
708
|
-
li.appendChild(checkbox);
|
|
709
|
-
li.appendChild(document.createTextNode(itemText || '\u200B'));
|
|
710
|
-
newUl.appendChild(li);
|
|
711
|
-
|
|
712
|
-
if (block && BLOCK_TAGS.has(block.tagName)) {
|
|
713
|
-
block.parentNode.replaceChild(newUl, block);
|
|
714
|
-
} else {
|
|
715
|
-
// Cursor directly in editable root — insert via Range API.
|
|
716
|
-
const nativeRange = sel.getRangeAt(0);
|
|
717
|
-
nativeRange.deleteContents();
|
|
718
|
-
nativeRange.insertNode(newUl);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Move caret to the text node inside the new <li>.
|
|
722
|
-
const textNode = li.lastChild;
|
|
723
|
-
const nr = document.createRange();
|
|
724
|
-
const offset = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent.length : 0;
|
|
725
|
-
nr.setStart(textNode, offset);
|
|
726
|
-
nr.collapse(true);
|
|
727
|
-
sel.removeAllRanges();
|
|
728
|
-
sel.addRange(nr);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Non-collapsed selection — convert each intersected block element into
|
|
733
|
-
// a checklist item using direct DOM manipulation.
|
|
734
|
-
const rawSelText = sel.toString().replace(/[\u00a0\u200B]/g, ' ').trim();
|
|
735
|
-
if (!rawSelText) return;
|
|
736
|
-
|
|
737
|
-
const BLOCK_TAGS_MULTI = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'LI']);
|
|
738
|
-
|
|
739
|
-
// Collect block-level ancestors of every node in the selection, in order.
|
|
740
|
-
const blocks = [];
|
|
741
|
-
const seenBlocks = new Set();
|
|
742
|
-
const commonAncestor = range.commonAncestorContainer;
|
|
743
|
-
const iter = document.createNodeIterator(
|
|
744
|
-
commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentNode : commonAncestor,
|
|
745
|
-
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
|
746
|
-
null,
|
|
747
|
-
);
|
|
748
|
-
let node;
|
|
749
|
-
while ((node = iter.nextNode())) {
|
|
750
|
-
if (!range.intersectsNode(node)) continue;
|
|
751
|
-
let blockEl = /** @type {Element|null} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
|
|
752
|
-
while (blockEl && blockEl !== editableRoot && !BLOCK_TAGS_MULTI.has(blockEl.tagName)) {
|
|
753
|
-
blockEl = blockEl.parentElement;
|
|
754
|
-
}
|
|
755
|
-
if (blockEl === editableRoot) blockEl = null;
|
|
756
|
-
if (blockEl && !seenBlocks.has(blockEl)) {
|
|
757
|
-
seenBlocks.add(blockEl);
|
|
758
|
-
blocks.push(blockEl);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
if (blocks.length === 0) return;
|
|
763
|
-
|
|
764
|
-
// Build checklist and replace collected blocks.
|
|
765
|
-
const newUl = document.createElement('ul');
|
|
766
|
-
newUl.className = 'an-checklist';
|
|
767
|
-
/** @type {Text|null} */ let lastTextNode = null;
|
|
768
|
-
blocks.forEach((block) => {
|
|
769
|
-
const li = document.createElement('li');
|
|
770
|
-
const cb = document.createElement('input');
|
|
771
|
-
cb.type = 'checkbox';
|
|
772
|
-
cb.contentEditable = 'false';
|
|
773
|
-
li.appendChild(cb);
|
|
774
|
-
// Preserve plain text content; ZWS/NBSP are stripped for display.
|
|
775
|
-
const blockText = Array.from(block.childNodes)
|
|
776
|
-
.map((n) => n.textContent)
|
|
777
|
-
.join('')
|
|
778
|
-
.replace(/[\u00a0\u200B]/g, ' ')
|
|
779
|
-
.trim();
|
|
780
|
-
const tn = document.createTextNode(blockText || '\u200B');
|
|
781
|
-
li.appendChild(tn);
|
|
782
|
-
newUl.appendChild(li);
|
|
783
|
-
lastTextNode = tn;
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
// Insert the new list before the first block, then remove all source blocks.
|
|
787
|
-
const firstBlock = blocks[0];
|
|
788
|
-
firstBlock.parentNode.insertBefore(newUl, firstBlock);
|
|
789
|
-
blocks.forEach((block) => block.remove());
|
|
790
|
-
|
|
791
|
-
// Move caret to end of the last checklist item.
|
|
792
|
-
if (lastTextNode) {
|
|
793
|
-
const nr = document.createRange();
|
|
794
|
-
nr.setStart(lastTextNode, lastTextNode.textContent.length);
|
|
795
|
-
nr.collapse(true);
|
|
796
|
-
sel.removeAllRanges();
|
|
797
|
-
sel.addRange(nr);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Returns true when the cursor is inside a checklist item.
|
|
804
|
-
* @returns {boolean}
|
|
805
|
-
*/
|
|
806
|
-
export function isInChecklist() {
|
|
807
|
-
const sel = globalThis.getSelection();
|
|
808
|
-
if (!sel?.rangeCount) return false;
|
|
809
|
-
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
810
|
-
if (container.nodeType === 3) container = container.parentElement;
|
|
811
|
-
return !!(/** @type {Element|null} */ (container)?.closest('.an-checklist li'));
|
|
812
|
-
}
|