autumnnote 1.8.0 → 1.8.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/README.md +17 -5
- package/dist/autumnnote.css +18 -1
- package/dist/autumnnote.es.js +350 -120
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +350 -120
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/js/core/markdown.js +32 -6
- package/src/js/editing/Style.js +288 -159
- package/src/js/editing/Table.js +10 -2
- package/src/js/editing/Typing.js +21 -4
- package/src/js/i18n/de.js +10 -0
- package/src/js/i18n/es.js +10 -0
- package/src/js/i18n/fr.js +10 -0
- package/src/js/i18n/ja.js +10 -0
- package/src/js/i18n/ko.js +10 -0
- package/src/js/i18n/vi.js +8 -0
- package/src/js/i18n/zh.js +10 -0
- package/src/js/index.js +1 -1
- package/src/js/module/Clipboard.js +1 -1
- package/src/js/module/ImageDialog.js +1 -0
- package/src/js/module/Mention.js +10 -1
- package/src/js/module/TableTooltip.js +21 -0
- package/src/styles/autumnnote.scss +20 -3
- package/types/index.d.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "WYSIWYG rich-text editor built with vanilla JavaScript — zero dependencies, no jQuery. Dark mode, @mention, markdown shortcuts, bubble toolbar. React and Vue 3 wrappers included.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"rtl",
|
|
79
79
|
"autosave"
|
|
80
80
|
],
|
|
81
|
-
"homepage": "https://
|
|
81
|
+
"homepage": "https://autumn.konexforge.com/",
|
|
82
82
|
"repository": {
|
|
83
83
|
"type": "git",
|
|
84
84
|
"url": "git+https://github.com/cmm-cmm/Autumn-Note.git"
|
package/src/js/core/markdown.js
CHANGED
|
@@ -89,7 +89,16 @@ function _domToMd(node, depth = 0) {
|
|
|
89
89
|
const items = Array.from(el.querySelectorAll(':scope > li'));
|
|
90
90
|
if (!items.length) return inner();
|
|
91
91
|
const indent = ' '.repeat(depth);
|
|
92
|
-
const
|
|
92
|
+
const isChecklist = el.classList.contains('an-checklist');
|
|
93
|
+
const lines = items.map((li) => {
|
|
94
|
+
let prefix = '- ';
|
|
95
|
+
if (isChecklist) {
|
|
96
|
+
const cb = /** @type {HTMLInputElement | null} */ (li.querySelector('input[type="checkbox"]'));
|
|
97
|
+
const checked = cb ? cb.checked : false;
|
|
98
|
+
prefix = checked ? '- [x] ' : '- [ ] ';
|
|
99
|
+
}
|
|
100
|
+
return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
|
|
101
|
+
}).join('\n');
|
|
93
102
|
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
94
103
|
}
|
|
95
104
|
case 'ol': {
|
|
@@ -130,7 +139,7 @@ function _domToMd(node, depth = 0) {
|
|
|
130
139
|
* @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
|
|
131
140
|
*/
|
|
132
141
|
export function isMarkdown(text) {
|
|
133
|
-
return /^#{1,6} \
|
|
142
|
+
return /^#{1,6} [^\s]|^[ \t]*[-*+] [^\s]|^[ \t]*\d+\. [^\s]|^> [^\s]|^```|^\*{2}[^*\n]+\*{2}/m.test(text);
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
/**
|
|
@@ -189,14 +198,31 @@ export function markdownToHTML(text) {
|
|
|
189
198
|
continue;
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
// ---- Unordered list - / * / + item
|
|
201
|
+
// ---- Checklist or Unordered list - / * / + item ----------------------
|
|
193
202
|
if (/^[-*+] /.test(line)) {
|
|
194
203
|
const items = [];
|
|
204
|
+
const isChecklist = /^[-*+]\s+\[[ xX]\]\s+/.test(line);
|
|
205
|
+
const listTag = isChecklist ? 'ul class="an-checklist"' : 'ul';
|
|
195
206
|
while (i < lines.length && /^[-*+] /.test(lines[i])) {
|
|
196
|
-
|
|
207
|
+
const nextLineIsChecklist = /^[-*+]\s+\[[ xX]\]\s+/.test(lines[i]);
|
|
208
|
+
if (nextLineIsChecklist !== isChecklist) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
const itemLine = lines[i];
|
|
212
|
+
const content = itemLine.slice(2);
|
|
213
|
+
if (isChecklist) {
|
|
214
|
+
const cbMatch = /^\[([ xX])\][ \t]+/.exec(content);
|
|
215
|
+
const checked = cbMatch?.[1]?.toLowerCase() === 'x';
|
|
216
|
+
const checkedAttr = checked ? ' checked' : '';
|
|
217
|
+
const cbHtml = `<input type="checkbox" contenteditable="false"${checkedAttr}>`;
|
|
218
|
+
const textContent = cbMatch ? content.slice(cbMatch[0].length) : content;
|
|
219
|
+
items.push(`<li>${cbHtml}${_inline(textContent)}</li>`);
|
|
220
|
+
} else {
|
|
221
|
+
items.push(`<li>${_inline(content)}</li>`);
|
|
222
|
+
}
|
|
197
223
|
i++;
|
|
198
224
|
}
|
|
199
|
-
out.push(
|
|
225
|
+
out.push(`<${listTag}>${items.join('')}</${listTag.split(' ')[0]}>`);
|
|
200
226
|
continue;
|
|
201
227
|
}
|
|
202
228
|
|
|
@@ -290,7 +316,7 @@ function _inline(text) {
|
|
|
290
316
|
text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
|
|
291
317
|
text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
|
|
292
318
|
// Strikethrough ~~text~~
|
|
293
|
-
text = text.replace(/~~([
|
|
319
|
+
text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
|
|
294
320
|
// Inline code `code`
|
|
295
321
|
text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
|
|
296
322
|
return text;
|
package/src/js/editing/Style.js
CHANGED
|
@@ -294,14 +294,99 @@ function _checklistItemToP(checkLi) {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
* Inserts an unordered list or converts
|
|
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
|
|
298
327
|
*/
|
|
299
|
-
|
|
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
|
+
}
|
|
300
354
|
|
|
301
355
|
/**
|
|
302
|
-
* Inserts an ordered list or converts
|
|
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')`.
|
|
303
370
|
*/
|
|
304
|
-
export
|
|
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
|
+
}
|
|
305
390
|
|
|
306
391
|
// ---------------------------------------------------------------------------
|
|
307
392
|
// Line-height helper
|
|
@@ -490,19 +575,42 @@ export function isInlineCode() {
|
|
|
490
575
|
// Checklist (task list)
|
|
491
576
|
// ---------------------------------------------------------------------------
|
|
492
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
|
+
|
|
493
612
|
/**
|
|
494
613
|
* Toggle a checklist at the current selection or caret.
|
|
495
|
-
*
|
|
496
|
-
* When the selection is inside an existing checklist `<ul class="an-checklist">`,
|
|
497
|
-
* converts the selected `<li>` items back into `<p>` paragraphs and places the caret
|
|
498
|
-
* at the start of the first converted paragraph. Otherwise creates a checklist:
|
|
499
|
-
* - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
|
|
500
|
-
* a single checklist item at the editable root) into a checklist with one item containing
|
|
501
|
-
* that block's text and places the caret inside the new item.
|
|
502
|
-
* - If the selection is a range, converts each intersecting block element into one checklist
|
|
503
|
-
* item (preserving textual content) and places the caret at the end of the last item.
|
|
504
|
-
*
|
|
505
|
-
* Empty or whitespace-only selections do not create a checklist.
|
|
506
614
|
*/
|
|
507
615
|
export function toggleChecklist() {
|
|
508
616
|
const sel = globalThis.getSelection();
|
|
@@ -511,162 +619,183 @@ export function toggleChecklist() {
|
|
|
511
619
|
let container = range.commonAncestorContainer;
|
|
512
620
|
if (container.nodeType === 3) container = container.parentElement;
|
|
513
621
|
|
|
514
|
-
const
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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);
|
|
532
652
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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) {
|
|
540
663
|
const nr = document.createRange();
|
|
541
|
-
nr.
|
|
542
|
-
nr.collapse(
|
|
664
|
+
nr.selectNodeContents(firstLi);
|
|
665
|
+
nr.collapse(false);
|
|
543
666
|
sel.removeAllRanges();
|
|
544
667
|
sel.addRange(nr);
|
|
545
668
|
}
|
|
546
|
-
return;
|
|
547
669
|
}
|
|
548
|
-
}
|
|
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
|
+
}
|
|
549
720
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
// Fallback: if no block element found (e.g. cursor directly in editable root), use the
|
|
561
|
-
// insertion approach with a zero-width-space item so the cursor ends up inside.
|
|
562
|
-
const itemText = (block && BLOCK_TAGS.has(block.tagName))
|
|
563
|
-
? Array.from(block.childNodes)
|
|
564
|
-
.map((n) => n.textContent)
|
|
565
|
-
.join('')
|
|
566
|
-
.replaceAll('\u00a0', ' ')
|
|
567
|
-
: '';
|
|
568
|
-
|
|
569
|
-
const ul = document.createElement('ul');
|
|
570
|
-
ul.className = 'an-checklist';
|
|
571
|
-
const li = document.createElement('li');
|
|
572
|
-
const checkbox = document.createElement('input');
|
|
573
|
-
checkbox.type = 'checkbox';
|
|
574
|
-
checkbox.contentEditable = 'false';
|
|
575
|
-
li.appendChild(checkbox);
|
|
576
|
-
li.appendChild(document.createTextNode(itemText || '\u200B'));
|
|
577
|
-
ul.appendChild(li);
|
|
578
|
-
|
|
579
|
-
if (block && BLOCK_TAGS.has(block.tagName)) {
|
|
580
|
-
block.parentNode.replaceChild(ul, block);
|
|
581
|
-
} else {
|
|
582
|
-
// Cursor directly in editable root — insert via Range API
|
|
583
|
-
const nativeRange = sel.getRangeAt(0);
|
|
584
|
-
nativeRange.deleteContents();
|
|
585
|
-
nativeRange.insertNode(ul);
|
|
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;
|
|
586
730
|
}
|
|
587
731
|
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const iter = document.createNodeIterator(
|
|
617
|
-
commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentNode : commonAncestor,
|
|
618
|
-
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
|
619
|
-
null,
|
|
620
|
-
);
|
|
621
|
-
let node;
|
|
622
|
-
while ((node = iter.nextNode())) {
|
|
623
|
-
if (!range.intersectsNode(node)) continue;
|
|
624
|
-
let block = /** @type {Element|null} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
|
|
625
|
-
while (block && !BLOCK_TAGS_MULTI.has(block.tagName)) {
|
|
626
|
-
block = block.parentElement;
|
|
627
|
-
}
|
|
628
|
-
if (block && !seenBlocks.has(block)) {
|
|
629
|
-
seenBlocks.add(block);
|
|
630
|
-
blocks.push(block);
|
|
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
|
+
}
|
|
631
760
|
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (blocks.length === 0) return;
|
|
635
|
-
|
|
636
|
-
// Build checklist and replace collected blocks.
|
|
637
|
-
const newUl = document.createElement('ul');
|
|
638
|
-
newUl.className = 'an-checklist';
|
|
639
|
-
/** @type {Text|null} */ let lastTextNode = null;
|
|
640
|
-
blocks.forEach((block) => {
|
|
641
|
-
const li = document.createElement('li');
|
|
642
|
-
const cb = document.createElement('input');
|
|
643
|
-
cb.type = 'checkbox';
|
|
644
|
-
cb.setAttribute('contenteditable', 'false');
|
|
645
|
-
li.appendChild(cb);
|
|
646
|
-
// Preserve plain text content; ZWS/NBSP are stripped for display.
|
|
647
|
-
const blockText = Array.from(block.childNodes)
|
|
648
|
-
.map((n) => n.textContent)
|
|
649
|
-
.join('')
|
|
650
|
-
.replace(/[\u00a0\u200B]/g, ' ')
|
|
651
|
-
.trim();
|
|
652
|
-
const tn = document.createTextNode(blockText || '\u200B');
|
|
653
|
-
li.appendChild(tn);
|
|
654
|
-
newUl.appendChild(li);
|
|
655
|
-
lastTextNode = tn;
|
|
656
|
-
});
|
|
657
761
|
|
|
658
|
-
|
|
659
|
-
const firstBlock = blocks[0];
|
|
660
|
-
firstBlock.parentNode.insertBefore(newUl, firstBlock);
|
|
661
|
-
blocks.forEach((block) => block.remove());
|
|
762
|
+
if (blocks.length === 0) return;
|
|
662
763
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
}
|
|
670
799
|
}
|
|
671
800
|
}
|
|
672
801
|
|
package/src/js/editing/Table.js
CHANGED
|
@@ -59,7 +59,11 @@ export function insertTable(cols, rows, opts = {}) {
|
|
|
59
59
|
const sel = globalThis.getSelection();
|
|
60
60
|
if (!sel || sel.rangeCount === 0) return;
|
|
61
61
|
const range = sel.getRangeAt(0);
|
|
62
|
-
|
|
62
|
+
try {
|
|
63
|
+
range.deleteContents();
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
// Walk up to find the nearest block-level ancestor to insert after
|
|
65
69
|
const BLOCK = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'PRE']);
|
|
@@ -82,7 +86,11 @@ export function insertTable(cols, rows, opts = {}) {
|
|
|
82
86
|
anchor.remove();
|
|
83
87
|
}
|
|
84
88
|
} else {
|
|
85
|
-
|
|
89
|
+
try {
|
|
90
|
+
range.insertNode(table);
|
|
91
|
+
} catch (_) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
// Place cursor in the first cell
|
package/src/js/editing/Typing.js
CHANGED
|
@@ -17,6 +17,25 @@ const _FA_PATTERN = /\bfa-/;
|
|
|
17
17
|
const isFAIcon = (n) => !!(n?.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
|
|
18
18
|
const isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the content from `startContainer:startOffset` to the end of `li`.
|
|
22
|
+
* Returns an empty fragment if the range is invalid (e.g. detached node).
|
|
23
|
+
* @param {Range} nativeRange
|
|
24
|
+
* @param {Element} li
|
|
25
|
+
* @returns {DocumentFragment}
|
|
26
|
+
*/
|
|
27
|
+
function extractAfterContent(nativeRange, li) {
|
|
28
|
+
try {
|
|
29
|
+
const r = document.createRange();
|
|
30
|
+
r.setStart(nativeRange.startContainer, nativeRange.startOffset);
|
|
31
|
+
r.setEnd(li, li.childNodes.length);
|
|
32
|
+
return r.extractContents();
|
|
33
|
+
} catch (_) {
|
|
34
|
+
void _;
|
|
35
|
+
return document.createDocumentFragment();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Handles special keydown behaviour inside the editor.
|
|
22
41
|
* @param {KeyboardEvent} event
|
|
@@ -312,16 +331,14 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
312
331
|
if (!nativeRange.collapsed) {
|
|
313
332
|
nativeRange.deleteContents();
|
|
314
333
|
// nativeRange is now collapsed at the deletion point; re-read it
|
|
334
|
+
if (sel.rangeCount === 0 || !checkLi.isConnected) return true;
|
|
315
335
|
nativeRange = sel.getRangeAt(0);
|
|
316
336
|
}
|
|
317
337
|
|
|
318
338
|
// 3. Extract everything from cursor to end of li into afterFrag.
|
|
319
339
|
// Use startContainer/startOffset (cursor position after potential delete),
|
|
320
340
|
// NOT endContainer/endOffset which is wrong for non-collapsed ranges.
|
|
321
|
-
const
|
|
322
|
-
afterRange.setStart(nativeRange.startContainer, nativeRange.startOffset);
|
|
323
|
-
afterRange.setEnd(checkLi, checkLi.childNodes.length);
|
|
324
|
-
const afterFrag = afterRange.extractContents();
|
|
341
|
+
const afterFrag = extractAfterContent(nativeRange, checkLi);
|
|
325
342
|
|
|
326
343
|
// 4. Build the new checklist item with the extracted "after" content.
|
|
327
344
|
const newLi = document.createElement('li');
|