autumnnote 1.0.6 → 1.0.8
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 +3 -3
- package/dist/autumnnote.css +168 -4
- package/dist/autumnnote.es.js +886 -115
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +886 -115
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +3 -2
- package/src/js/Context.js +6 -0
- package/src/js/core/sanitise.js +20 -2
- package/src/js/editing/Style.js +288 -24
- package/src/js/editing/Typing.js +2 -2
- package/src/js/module/Buttons.js +5 -4
- package/src/js/module/Clipboard.js +8 -0
- package/src/js/module/CodeTooltip.js +1 -0
- package/src/js/module/ContextMenu.js +146 -8
- package/src/js/module/Editor.js +73 -1
- package/src/js/module/EmojiDialog.js +11 -0
- package/src/js/module/IconDialog.js +11 -0
- package/src/js/module/ImageCropOverlay.js +2 -2
- package/src/js/module/ImageDialog.js +22 -3
- package/src/js/module/ImageResizer.js +2 -0
- package/src/js/module/ImageTooltip.js +9 -0
- package/src/js/module/LinkTooltip.js +4 -0
- package/src/js/module/Placeholder.js +7 -1
- package/src/js/module/TableTooltip.js +399 -86
- package/src/js/module/Toolbar.js +21 -3
- package/src/js/module/VideoDialog.js +5 -5
- package/src/js/module/VideoResizer.js +11 -0
- package/src/js/module/VideoTooltip.js +1 -0
- package/src/js/renderer.js +3 -0
- package/src/styles/autumnnote.scss +194 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A modern, lightweight WYSIWYG editor — built with vanilla JavaScript, no jQuery required.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
|
@@ -45,9 +45,10 @@
|
|
|
45
45
|
"license": "MIT",
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@vitest/browser": "^4.1.2",
|
|
48
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
48
49
|
"cross-env": "^10.1.0",
|
|
49
50
|
"eslint": "^10.2.0",
|
|
50
|
-
"jsdom": "^
|
|
51
|
+
"jsdom": "^25.0.1",
|
|
51
52
|
"rollup-plugin-visualizer": "^7.0.1",
|
|
52
53
|
"sass": "^1.99.0",
|
|
53
54
|
"typescript": "^6.0.2",
|
package/src/js/Context.js
CHANGED
|
@@ -444,9 +444,15 @@ export class Context {
|
|
|
444
444
|
if (disabled) {
|
|
445
445
|
editable.setAttribute('contenteditable', 'false');
|
|
446
446
|
this.layoutInfo.container.classList.add('an-disabled');
|
|
447
|
+
editable.querySelectorAll('ul.an-checklist input[type="checkbox"]').forEach((cb) => {
|
|
448
|
+
cb.setAttribute('disabled', '');
|
|
449
|
+
});
|
|
447
450
|
} else {
|
|
448
451
|
editable.setAttribute('contenteditable', 'true');
|
|
449
452
|
this.layoutInfo.container.classList.remove('an-disabled');
|
|
453
|
+
editable.querySelectorAll('ul.an-checklist input[type="checkbox"]').forEach((cb) => {
|
|
454
|
+
cb.removeAttribute('disabled');
|
|
455
|
+
});
|
|
450
456
|
}
|
|
451
457
|
}
|
|
452
458
|
|
package/src/js/core/sanitise.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** Tags that are unconditionally removed from editor content. */
|
|
9
|
-
const PROHIBITED_TAGS = ['script', 'style', 'iframe', 'object', 'embed', 'form', '
|
|
9
|
+
const PROHIBITED_TAGS = ['script', 'style', 'iframe', 'object', 'embed', 'form', 'button'];
|
|
10
10
|
|
|
11
11
|
/** Attributes whose values must be sanitised as URLs. */
|
|
12
12
|
const URL_ATTRS = ['href', 'src', 'action', 'formaction'];
|
|
@@ -26,7 +26,8 @@ const TRUSTED_IFRAME_HOSTS = new Set([
|
|
|
26
26
|
* Uses DOMParser so the sanitisation follows normal browser parsing rules —
|
|
27
27
|
* no regex shortcuts that can be bypassed by encoding tricks.
|
|
28
28
|
*
|
|
29
|
-
* - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form,
|
|
29
|
+
* - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, button)
|
|
30
|
+
* - Allows input[type="checkbox"] only inside ul.an-checklist li; removes all other <input>
|
|
30
31
|
* - Removes all on* event-handler attributes
|
|
31
32
|
* - Rejects javascript: and vbscript: URLs in URL attributes
|
|
32
33
|
* - Rejects data: URIs everywhere except img[src] (base64 uploads)
|
|
@@ -81,6 +82,23 @@ export function sanitiseHTML(html, { allowIframes = false } = {}) {
|
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
|
|
85
|
+
// Allow only input[type="checkbox"] inside ul.an-checklist li; strip everything else.
|
|
86
|
+
// This preserves checklist state while blocking arbitrary <input> injection.
|
|
87
|
+
doc.querySelectorAll('input').forEach((el) => {
|
|
88
|
+
const inChecklist = el.closest('ul.an-checklist') !== null &&
|
|
89
|
+
el.closest('li') !== null;
|
|
90
|
+
if (!inChecklist || el.getAttribute('type') !== 'checkbox') {
|
|
91
|
+
el.remove();
|
|
92
|
+
} else {
|
|
93
|
+
// Harden: keep only safe attributes on checklist checkboxes
|
|
94
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
95
|
+
if (!['type', 'checked', 'contenteditable'].includes(attr.name)) {
|
|
96
|
+
el.removeAttribute(attr.name);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
84
102
|
return doc.body.innerHTML;
|
|
85
103
|
}
|
|
86
104
|
|
package/src/js/editing/Style.js
CHANGED
|
@@ -60,8 +60,29 @@ export function underline() {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
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).
|
|
63
65
|
*/
|
|
64
|
-
export
|
|
66
|
+
export function strikethrough() {
|
|
67
|
+
const sel = window.getSelection();
|
|
68
|
+
if (!sel || !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 = sc && sc.closest && (sc.closest('s') || 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
|
+
parent.removeChild(sEl);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
execCommand('strikeThrough');
|
|
85
|
+
}
|
|
65
86
|
|
|
66
87
|
/**
|
|
67
88
|
* Superscript toggle.
|
|
@@ -98,15 +119,65 @@ export const fontName = (name) => execCommand('fontName', name);
|
|
|
98
119
|
* @param {HTMLElement} [editable] - scoping element to avoid touching nodes outside this editor
|
|
99
120
|
*/
|
|
100
121
|
export function fontSize(size, editable = document) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
const sel = window.getSelection();
|
|
123
|
+
const wasCollapsed = !sel || !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 && 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 (_) { /* 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) => {
|
|
104
157
|
const span = document.createElement('span');
|
|
105
158
|
span.style.fontSize = size;
|
|
106
159
|
el.parentNode.insertBefore(span, el);
|
|
107
160
|
while (el.firstChild) span.appendChild(el.firstChild);
|
|
108
161
|
el.parentNode.removeChild(el);
|
|
162
|
+
newSpans.push(span);
|
|
109
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[newSpans.length - 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 (_) { /* ignore range errors on unusual DOM structures */ }
|
|
180
|
+
}
|
|
110
181
|
}
|
|
111
182
|
|
|
112
183
|
// ---------------------------------------------------------------------------
|
|
@@ -146,8 +217,69 @@ export const indent = () => execCommand('indent');
|
|
|
146
217
|
|
|
147
218
|
/**
|
|
148
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).
|
|
149
223
|
*/
|
|
150
|
-
export
|
|
224
|
+
export function outdent() {
|
|
225
|
+
const sel = window.getSelection();
|
|
226
|
+
if (sel && sel.rangeCount) {
|
|
227
|
+
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
228
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
229
|
+
const checkLi = container && container.closest && container.closest('.an-checklist li');
|
|
230
|
+
if (checkLi) {
|
|
231
|
+
_checklistItemToP(checkLi);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
execCommand('outdent');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* G.5 helper: splits a checklist at checkLi, converts it to a <p>,
|
|
240
|
+
* and keeps items before/after as separate checklists.
|
|
241
|
+
* @param {HTMLElement} checkLi
|
|
242
|
+
*/
|
|
243
|
+
function _checklistItemToP(checkLi) {
|
|
244
|
+
const checkUl = checkLi.closest('.an-checklist');
|
|
245
|
+
if (!checkUl) return;
|
|
246
|
+
|
|
247
|
+
const allLis = Array.from(checkUl.children);
|
|
248
|
+
const liIndex = allLis.indexOf(checkLi);
|
|
249
|
+
const afterLis = allLis.slice(liIndex + 1);
|
|
250
|
+
|
|
251
|
+
// Build <p> from the item's text (skip the checkbox INPUT)
|
|
252
|
+
const p = document.createElement('p');
|
|
253
|
+
const text = Array.from(checkLi.childNodes)
|
|
254
|
+
.filter(n => !(n.nodeType === 1 && n.tagName === 'INPUT'))
|
|
255
|
+
.map(n => n.textContent).join('').replace(/\u200B/g, '').trim();
|
|
256
|
+
p.textContent = text || '\u00a0';
|
|
257
|
+
|
|
258
|
+
// Move items after the current li into a new checklist
|
|
259
|
+
if (afterLis.length > 0) {
|
|
260
|
+
const newUl = document.createElement('ul');
|
|
261
|
+
newUl.className = 'an-checklist';
|
|
262
|
+
afterLis.forEach(li => newUl.appendChild(li));
|
|
263
|
+
checkUl.parentNode.insertBefore(newUl, checkUl.nextSibling);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Insert <p> after checkUl (before any newUl)
|
|
267
|
+
checkUl.parentNode.insertBefore(p, checkUl.nextSibling);
|
|
268
|
+
|
|
269
|
+
// Remove current li from checkUl; delete checkUl if now empty
|
|
270
|
+
checkUl.removeChild(checkLi);
|
|
271
|
+
if (checkUl.children.length === 0) checkUl.parentNode.removeChild(checkUl);
|
|
272
|
+
|
|
273
|
+
// Place caret at start of the new <p>
|
|
274
|
+
try {
|
|
275
|
+
const nr = document.createRange();
|
|
276
|
+
const firstChild = p.firstChild;
|
|
277
|
+
nr.setStart(firstChild && firstChild.nodeType === 3 ? firstChild : p, 0);
|
|
278
|
+
nr.collapse(true);
|
|
279
|
+
const s = window.getSelection();
|
|
280
|
+
if (s) { s.removeAllRanges(); s.addRange(nr); }
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
151
283
|
|
|
152
284
|
/**
|
|
153
285
|
* Inserts an unordered list or converts selection.
|
|
@@ -263,11 +395,36 @@ export function toggleInlineCode(editable) {
|
|
|
263
395
|
if (container.nodeType === 3) container = container.parentElement;
|
|
264
396
|
const codeEl = container && container.closest ? container.closest('code') : null;
|
|
265
397
|
if (codeEl && !codeEl.closest('pre')) {
|
|
266
|
-
// Unwrap
|
|
398
|
+
// Unwrap — save range endpoints relative to surrounding text so we can
|
|
399
|
+
// restore the selection after normalize() merges adjacent text nodes.
|
|
267
400
|
const parent = codeEl.parentNode;
|
|
401
|
+
// Note the sibling before the code element so we can re-anchor later.
|
|
402
|
+
const prevSibling = codeEl.previousSibling;
|
|
403
|
+
const movedChildren = Array.from(codeEl.childNodes);
|
|
268
404
|
while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
|
|
269
405
|
parent.removeChild(codeEl);
|
|
270
|
-
|
|
406
|
+
// Normalize only the immediate parent to merge adjacent text nodes without
|
|
407
|
+
// invalidating distant selection anchors (full editable.normalize() can
|
|
408
|
+
// cause selection offsets to shift, making subsequent format toggles miss).
|
|
409
|
+
if (parent && parent.normalize) parent.normalize();
|
|
410
|
+
// Restore selection to the text that was inside the unwrapped <code>.
|
|
411
|
+
if (movedChildren.length > 0) {
|
|
412
|
+
try {
|
|
413
|
+
// After normalize, find the merged text node that contains the content.
|
|
414
|
+
const firstMoved = movedChildren[0];
|
|
415
|
+
const lastMoved = movedChildren[movedChildren.length - 1];
|
|
416
|
+
const nr = document.createRange();
|
|
417
|
+
// Use the (possibly merged) live node if still in the DOM.
|
|
418
|
+
const anchorNode = (firstMoved.parentNode === parent) ? firstMoved : (prevSibling ? prevSibling.nextSibling : parent.firstChild);
|
|
419
|
+
if (anchorNode) {
|
|
420
|
+
nr.setStart(anchorNode, 0);
|
|
421
|
+
const endAnchor = (lastMoved.parentNode === parent) ? lastMoved : anchorNode;
|
|
422
|
+
nr.setEnd(endAnchor, endAnchor.nodeType === Node.TEXT_NODE ? endAnchor.textContent.length : endAnchor.childNodes.length);
|
|
423
|
+
sel.removeAllRanges();
|
|
424
|
+
sel.addRange(nr);
|
|
425
|
+
}
|
|
426
|
+
} catch (_) { /* ignore */ }
|
|
427
|
+
}
|
|
271
428
|
} else {
|
|
272
429
|
if (range.collapsed) return;
|
|
273
430
|
try {
|
|
@@ -296,14 +453,17 @@ export function toggleInlineCode(editable) {
|
|
|
296
453
|
/**
|
|
297
454
|
* Returns true when the cursor / selection is inside an inline <code>
|
|
298
455
|
* (not nested in a <pre>).
|
|
456
|
+
* Uses startContainer for reliable cross-browser detection regardless of
|
|
457
|
+
* whether the selection is collapsed or a range (commonAncestorContainer
|
|
458
|
+
* can behave inconsistently for range selections on some browsers).
|
|
299
459
|
* @returns {boolean}
|
|
300
460
|
*/
|
|
301
461
|
export function isInlineCode() {
|
|
302
462
|
const sel = window.getSelection();
|
|
303
463
|
if (!sel || !sel.rangeCount) return false;
|
|
304
|
-
let
|
|
305
|
-
if (
|
|
306
|
-
const code =
|
|
464
|
+
let sc = sel.getRangeAt(0).startContainer;
|
|
465
|
+
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
466
|
+
const code = sc && sc.closest ? sc.closest('code') : null;
|
|
307
467
|
return !!(code && !code.closest('pre'));
|
|
308
468
|
}
|
|
309
469
|
|
|
@@ -357,21 +517,125 @@ export function toggleChecklist() {
|
|
|
357
517
|
}
|
|
358
518
|
}
|
|
359
519
|
|
|
360
|
-
// Otherwise: insert new checklist from selected text
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
520
|
+
// Otherwise: insert new checklist from selected text (or current block when collapsed).
|
|
521
|
+
const isCollapsed = range.collapsed;
|
|
522
|
+
if (isCollapsed) {
|
|
523
|
+
// Find the nearest block-level ancestor (p, div, li, h1-h6, blockquote, etc.)
|
|
524
|
+
// and convert it into a single checklist item.
|
|
525
|
+
const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI']);
|
|
526
|
+
let block = container;
|
|
527
|
+
while (block && block.parentNode && !BLOCK_TAGS.has(block.tagName)) {
|
|
528
|
+
block = block.parentNode;
|
|
529
|
+
}
|
|
530
|
+
// Fallback: if no block element found (e.g. cursor directly in editable root), use the
|
|
531
|
+
// insertion approach with a zero-width-space item so the cursor ends up inside.
|
|
532
|
+
const itemText = (block && BLOCK_TAGS.has(block.tagName))
|
|
533
|
+
? Array.from(block.childNodes)
|
|
534
|
+
.map((n) => n.textContent)
|
|
535
|
+
.join('')
|
|
536
|
+
.replace(/\u00a0/g, ' ')
|
|
537
|
+
: '';
|
|
538
|
+
|
|
539
|
+
const ul = document.createElement('ul');
|
|
540
|
+
ul.className = 'an-checklist';
|
|
541
|
+
const li = document.createElement('li');
|
|
542
|
+
const checkbox = document.createElement('input');
|
|
543
|
+
checkbox.type = 'checkbox';
|
|
544
|
+
checkbox.contentEditable = 'false';
|
|
545
|
+
li.appendChild(checkbox);
|
|
546
|
+
li.appendChild(document.createTextNode(itemText || '\u200B'));
|
|
547
|
+
ul.appendChild(li);
|
|
548
|
+
|
|
549
|
+
if (block && BLOCK_TAGS.has(block.tagName)) {
|
|
550
|
+
block.parentNode.replaceChild(ul, block);
|
|
551
|
+
} else {
|
|
552
|
+
document.execCommand('insertHTML', false, ul.outerHTML);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Move caret to the text node inside the new <li>
|
|
557
|
+
const textNode = li.lastChild;
|
|
558
|
+
const nr = document.createRange();
|
|
559
|
+
const offset = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent.length : 0;
|
|
560
|
+
nr.setStart(textNode, offset);
|
|
561
|
+
nr.collapse(true);
|
|
562
|
+
sel.removeAllRanges();
|
|
563
|
+
sel.addRange(nr);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// G-4: Non-collapsed, non-checklist selection — convert each intersected
|
|
568
|
+
// block element into a checklist item using direct DOM manipulation.
|
|
569
|
+
// execCommand('insertHTML') is avoided here because in modern browsers it
|
|
570
|
+
// deletes the selection but may silently fail to insert when the selection
|
|
571
|
+
// spans multiple block elements, causing text to disappear.
|
|
572
|
+
|
|
573
|
+
// Guard: if the raw selection is entirely whitespace, do nothing (mirrors
|
|
574
|
+
// the old line-filter behaviour that prevented empty checklist creation).
|
|
575
|
+
const rawSelText = sel.toString().replace(/[\u00a0\u200B]/g, ' ').trim();
|
|
576
|
+
if (!rawSelText) return;
|
|
577
|
+
|
|
578
|
+
const BLOCK_TAGS_MULTI = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'LI']);
|
|
579
|
+
|
|
580
|
+
// Collect block-level ancestors of every node in the selection, in order.
|
|
581
|
+
const blocks = [];
|
|
582
|
+
const seenBlocks = new Set();
|
|
583
|
+
const commonAncestor = range.commonAncestorContainer;
|
|
584
|
+
const iter = document.createNodeIterator(
|
|
585
|
+
commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentNode : commonAncestor,
|
|
586
|
+
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
|
587
|
+
null,
|
|
374
588
|
);
|
|
589
|
+
let node;
|
|
590
|
+
while ((node = iter.nextNode())) {
|
|
591
|
+
if (!range.intersectsNode(node)) continue;
|
|
592
|
+
let block = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
|
593
|
+
while (block && !BLOCK_TAGS_MULTI.has(block.tagName)) {
|
|
594
|
+
block = block.parentElement;
|
|
595
|
+
}
|
|
596
|
+
if (block && !seenBlocks.has(block)) {
|
|
597
|
+
seenBlocks.add(block);
|
|
598
|
+
blocks.push(block);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (blocks.length === 0) return;
|
|
603
|
+
|
|
604
|
+
// Build checklist and replace collected blocks.
|
|
605
|
+
const newUl = document.createElement('ul');
|
|
606
|
+
newUl.className = 'an-checklist';
|
|
607
|
+
let lastTextNode = null;
|
|
608
|
+
blocks.forEach((block) => {
|
|
609
|
+
const li = document.createElement('li');
|
|
610
|
+
const cb = document.createElement('input');
|
|
611
|
+
cb.type = 'checkbox';
|
|
612
|
+
cb.setAttribute('contenteditable', 'false');
|
|
613
|
+
li.appendChild(cb);
|
|
614
|
+
// Preserve plain text content; ZWS/NBSP are stripped for display.
|
|
615
|
+
const blockText = Array.from(block.childNodes)
|
|
616
|
+
.map((n) => n.textContent)
|
|
617
|
+
.join('')
|
|
618
|
+
.replace(/[\u00a0\u200B]/g, ' ')
|
|
619
|
+
.trim();
|
|
620
|
+
const tn = document.createTextNode(blockText || '\u200B');
|
|
621
|
+
li.appendChild(tn);
|
|
622
|
+
newUl.appendChild(li);
|
|
623
|
+
lastTextNode = tn;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Insert the new list before the first block, then remove all source blocks.
|
|
627
|
+
const firstBlock = blocks[0];
|
|
628
|
+
firstBlock.parentNode.insertBefore(newUl, firstBlock);
|
|
629
|
+
blocks.forEach((block) => block.parentNode && block.parentNode.removeChild(block));
|
|
630
|
+
|
|
631
|
+
// Move caret to end of the last checklist item.
|
|
632
|
+
if (lastTextNode) {
|
|
633
|
+
const nr = document.createRange();
|
|
634
|
+
nr.setStart(lastTextNode, lastTextNode.textContent.length);
|
|
635
|
+
nr.collapse(true);
|
|
636
|
+
sel.removeAllRanges();
|
|
637
|
+
sel.addRange(nr);
|
|
638
|
+
}
|
|
375
639
|
}
|
|
376
640
|
|
|
377
641
|
/**
|
package/src/js/editing/Typing.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { key, isKey } from '../core/key.js';
|
|
7
7
|
import { closestPara, isLi } from '../core/dom.js';
|
|
8
|
-
import { execCommand } from './Style.js';
|
|
8
|
+
import { execCommand, outdent } from './Style.js';
|
|
9
9
|
import { currentRange } from '../core/range.js';
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
@@ -199,7 +199,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
199
199
|
if (para && isLi(para)) {
|
|
200
200
|
event.preventDefault();
|
|
201
201
|
if (event.shiftKey) {
|
|
202
|
-
|
|
202
|
+
outdent();
|
|
203
203
|
} else {
|
|
204
204
|
execCommand('indent');
|
|
205
205
|
}
|
package/src/js/module/Buttons.js
CHANGED
|
@@ -59,13 +59,14 @@ export const boldBtn = btn('bold', 'bold', 'Bold (Ctrl+B)', () => Style.bold(),
|
|
|
59
59
|
export const italicBtn = btn('italic', 'italic', 'Italic (Ctrl+I)', () => Style.italic(), () => document.queryCommandState('italic'));
|
|
60
60
|
export const underlineBtn = btn('underline', 'underline', 'Underline (Ctrl+U)', () => Style.underline(), () => {
|
|
61
61
|
// queryCommandState('underline') is unreliable inside <code> elements;
|
|
62
|
-
// also check for a <u> ancestor in the DOM
|
|
62
|
+
// also check for a <u> ancestor in the DOM using startContainer for
|
|
63
|
+
// consistent behaviour across both collapsed and range selections.
|
|
63
64
|
if (document.queryCommandState('underline')) return true;
|
|
64
65
|
const sel = window.getSelection();
|
|
65
66
|
if (!sel || !sel.rangeCount) return false;
|
|
66
|
-
let
|
|
67
|
-
if (
|
|
68
|
-
return !!(
|
|
67
|
+
let sc = sel.getRangeAt(0).startContainer;
|
|
68
|
+
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
69
|
+
return !!(sc && sc.closest && sc.closest('u'));
|
|
69
70
|
});
|
|
70
71
|
export const strikeBtn = btn('strikethrough', 'strikethrough', 'Strikethrough', () => Style.strikethrough(), () => document.queryCommandState('strikeThrough'));
|
|
71
72
|
export const superscriptBtn = btn('superscript', 'superscript', 'Superscript', () => Style.superscript(), () => document.queryCommandState('superscript'));
|
|
@@ -299,9 +299,17 @@ export class Clipboard {
|
|
|
299
299
|
return;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// C2: Reject image formats that browsers cannot decode/display.
|
|
303
|
+
const UNSUPPORTED = ['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp'];
|
|
302
304
|
const maxBytes = (this.options.maxImageSize || 5) * 1024 * 1024;
|
|
303
305
|
files.forEach((file) => {
|
|
304
306
|
if (!file || !file.type.startsWith('image/')) return;
|
|
307
|
+
if (UNSUPPORTED.includes(file.type)) {
|
|
308
|
+
const message = `Image format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
|
|
309
|
+
this.context.triggerEvent('imageError', { file, message });
|
|
310
|
+
console.warn('[AutumnNote]', message);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
305
313
|
if (file.size > maxBytes) {
|
|
306
314
|
const message = `Image "${file.name}" exceeds the ${this.options.maxImageSize || 5} MB size limit.`;
|
|
307
315
|
this.context.triggerEvent('imageError', { file, message });
|
|
@@ -39,6 +39,7 @@ export class CodeTooltip {
|
|
|
39
39
|
|
|
40
40
|
this._disposers.push(
|
|
41
41
|
on(editable, 'mouseover', (e) => {
|
|
42
|
+
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
42
43
|
const pre = e.target.closest('pre');
|
|
43
44
|
if (pre && editable.contains(pre)) {
|
|
44
45
|
this._scheduleShow(pre);
|