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/Table.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Table.js - Table creation and manipulation utilities
|
|
3
|
-
* Inspired by Summernote's table handling
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createElement } from '../core/dom.js';
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Table creation
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Build an HTML table with the given number of columns and rows, optionally including a header row.
|
|
14
|
-
* @param {number} cols - Number of columns in each row.
|
|
15
|
-
* @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
|
|
16
|
-
* @param {{ headerRow?: boolean }} [opts] - Options: `headerRow` creates a `<thead>` when true.
|
|
17
|
-
* @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
|
|
18
|
-
*/
|
|
19
|
-
export function createTable(cols, rows, opts = {}) {
|
|
20
|
-
const { headerRow = false } = opts;
|
|
21
|
-
const table = createElement('table', { class: 'an-table' });
|
|
22
|
-
|
|
23
|
-
if (headerRow && rows > 0) {
|
|
24
|
-
const thead = createElement('thead');
|
|
25
|
-
const tr = createElement('tr');
|
|
26
|
-
for (let c = 0; c < cols; c++) {
|
|
27
|
-
const th = createElement('th', {}, [document.createElement('br')]);
|
|
28
|
-
tr.appendChild(th);
|
|
29
|
-
}
|
|
30
|
-
thead.appendChild(tr);
|
|
31
|
-
table.appendChild(thead);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const bodyRows = headerRow ? Math.max(rows - 1, 1) : rows;
|
|
35
|
-
const tbody = createElement('tbody');
|
|
36
|
-
table.appendChild(tbody);
|
|
37
|
-
|
|
38
|
-
for (let r = 0; r < bodyRows; r++) {
|
|
39
|
-
const tr = createElement('tr');
|
|
40
|
-
for (let c = 0; c < cols; c++) {
|
|
41
|
-
const td = createElement('td', {}, [document.createElement('br')]);
|
|
42
|
-
tr.appendChild(td);
|
|
43
|
-
}
|
|
44
|
-
tbody.appendChild(tr);
|
|
45
|
-
}
|
|
46
|
-
return /** @type {HTMLTableElement} */ (table);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Insert a table at the current selection and place the caret into its first cell.
|
|
51
|
-
* @param {number} cols - Number of columns for the new table.
|
|
52
|
-
* @param {number} rows - Number of rows for the new table.
|
|
53
|
-
* @param {{ headerRow?: boolean }} [opts] - Options for table creation.
|
|
54
|
-
*/
|
|
55
|
-
export function insertTable(cols, rows, opts = {}) {
|
|
56
|
-
if (cols <= 0 || rows <= 0) return;
|
|
57
|
-
const table = createTable(cols, rows, opts);
|
|
58
|
-
|
|
59
|
-
const sel = globalThis.getSelection();
|
|
60
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
61
|
-
const range = sel.getRangeAt(0);
|
|
62
|
-
try {
|
|
63
|
-
range.deleteContents();
|
|
64
|
-
} catch (_) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Walk up to find the nearest block-level ancestor to insert after
|
|
69
|
-
const BLOCK = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'PRE']);
|
|
70
|
-
let anchor = /** @type {Element|null} */ (range.startContainer);
|
|
71
|
-
if (anchor?.nodeType === 3) anchor = anchor.parentElement;
|
|
72
|
-
while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) {
|
|
73
|
-
anchor = anchor.parentElement;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
|
|
77
|
-
anchor.after(table);
|
|
78
|
-
// Ensure there's a paragraph after the table for cursor landing
|
|
79
|
-
if (!table.nextElementSibling) {
|
|
80
|
-
const p = document.createElement('p');
|
|
81
|
-
p.appendChild(document.createElement('br'));
|
|
82
|
-
table.after(p);
|
|
83
|
-
}
|
|
84
|
-
// Remove the anchor block if it was empty (common case: cursor in blank paragraph)
|
|
85
|
-
if (!anchor.textContent.trim() && !anchor.querySelector('img, video, table')) {
|
|
86
|
-
anchor.remove();
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
try {
|
|
90
|
-
range.insertNode(table);
|
|
91
|
-
} catch (_) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Place cursor in the first cell
|
|
97
|
-
const firstCell = table.querySelector('td, th');
|
|
98
|
-
if (firstCell) {
|
|
99
|
-
const nr = document.createRange();
|
|
100
|
-
nr.setStart(firstCell, 0);
|
|
101
|
-
nr.collapse(true);
|
|
102
|
-
sel.removeAllRanges();
|
|
103
|
-
sel.addRange(nr);
|
|
104
|
-
}
|
|
105
|
-
}
|
package/src/js/editing/Typing.js
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typing.js - Keyboard typing event handling (Enter, Tab, Backspace behaviour)
|
|
3
|
-
* Inspired by Summernote's Typing module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { key, isKey } from '../core/key.js';
|
|
7
|
-
import { closestPara, isLi } from '../core/dom.js';
|
|
8
|
-
import { execCommand, outdent } from './Style.js';
|
|
9
|
-
import { currentRange } from '../core/range.js';
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Module-level predicates — defined once, not re-created on every keypress.
|
|
13
|
-
// Previously these were arrow functions inside handleKeydown() which fires
|
|
14
|
-
// at ~120+ events/sec during normal typing.
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
const _FA_PATTERN = /\bfa-/;
|
|
17
|
-
const isFAIcon = (n) => !!(n?.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
|
|
18
|
-
const isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
|
|
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
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Handles special keydown behaviour inside the editor.
|
|
41
|
-
* @param {KeyboardEvent} event
|
|
42
|
-
* @param {HTMLElement} editable
|
|
43
|
-
* @param {object} options - editor options
|
|
44
|
-
* @returns {boolean} true if the event was consumed
|
|
45
|
-
*/
|
|
46
|
-
export function handleKeydown(event, editable, options = {}) {
|
|
47
|
-
const moveCaret = (setFn) => {
|
|
48
|
-
const sel = globalThis.getSelection();
|
|
49
|
-
if (!sel) return false;
|
|
50
|
-
const nr = document.createRange();
|
|
51
|
-
setFn(nr);
|
|
52
|
-
nr.collapse(true);
|
|
53
|
-
sel.removeAllRanges();
|
|
54
|
-
sel.addRange(nr);
|
|
55
|
-
return true;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// -------------------------------------------------------------------------
|
|
59
|
-
// Backspace key — one-press deletion of a preceding FA icon (<i> element)
|
|
60
|
-
// -------------------------------------------------------------------------
|
|
61
|
-
if (isKey(event, key.BACKSPACE)) {
|
|
62
|
-
const sel = globalThis.getSelection();
|
|
63
|
-
if (sel?.rangeCount > 0) {
|
|
64
|
-
const r = sel.getRangeAt(0);
|
|
65
|
-
if (r.collapsed && r.startContainer.nodeType === Node.TEXT_NODE) {
|
|
66
|
-
const textNode = /** @type {ChildNode} */ (r.startContainer);
|
|
67
|
-
// Case A: cursor at offset 0, preceding sibling is an FA icon
|
|
68
|
-
if (r.startOffset === 0 && isFAIcon(textNode.previousSibling)) {
|
|
69
|
-
event.preventDefault();
|
|
70
|
-
/** @type {ChildNode} */ (textNode.previousSibling).remove();
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Case B: cursor at offset 1 of a ZWS-only text node whose preceding
|
|
75
|
-
// sibling is an FA icon. The ZWS is the invisible caret anchor inserted
|
|
76
|
-
// by IconDialog; treat the whole Backspace as "delete icon + its anchor".
|
|
77
|
-
if (r.startOffset === 1 && textNode.textContent === '\u200B' &&
|
|
78
|
-
isFAIcon(textNode.previousSibling)) {
|
|
79
|
-
event.preventDefault();
|
|
80
|
-
const parent = textNode.parentNode;
|
|
81
|
-
const icon = /** @type {ChildNode} */ (textNode.previousSibling);
|
|
82
|
-
const prevNode = icon.previousSibling; // node before the icon (e.g. ZWS of prior icon)
|
|
83
|
-
icon.remove();
|
|
84
|
-
textNode.remove();
|
|
85
|
-
// Explicitly restore the cursor to the node preceding the deleted icon.
|
|
86
|
-
// Without this, the browser collapses the selection to the parent element
|
|
87
|
-
// (not a text node), causing the next Backspace to miss Cases A/B and
|
|
88
|
-
// requiring an extra keypress when two icons are adjacent.
|
|
89
|
-
const nr = document.createRange();
|
|
90
|
-
if (prevNode?.nodeType === Node.TEXT_NODE) {
|
|
91
|
-
nr.setStart(prevNode, prevNode.textContent.length);
|
|
92
|
-
} else if (prevNode) {
|
|
93
|
-
nr.setStartAfter(prevNode);
|
|
94
|
-
} else if (parent) {
|
|
95
|
-
nr.setStart(parent, 0);
|
|
96
|
-
}
|
|
97
|
-
nr.collapse(true);
|
|
98
|
-
sel.removeAllRanges();
|
|
99
|
-
sel.addRange(nr);
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// -------------------------------------------------------------------------
|
|
108
|
-
// ArrowLeft / ArrowRight — one-press navigation across FA icon nodes
|
|
109
|
-
// -------------------------------------------------------------------------
|
|
110
|
-
if (isKey(event, key.LEFT) || isKey(event, key.RIGHT)) {
|
|
111
|
-
const sel = globalThis.getSelection();
|
|
112
|
-
if (!sel || sel.rangeCount === 0) return false;
|
|
113
|
-
|
|
114
|
-
const r = sel.getRangeAt(0);
|
|
115
|
-
if (!r.collapsed) return false;
|
|
116
|
-
|
|
117
|
-
const sc = r.startContainer;
|
|
118
|
-
const movingLeft = isKey(event, key.LEFT);
|
|
119
|
-
|
|
120
|
-
if (sc.nodeType === Node.TEXT_NODE) {
|
|
121
|
-
const textNode = sc;
|
|
122
|
-
|
|
123
|
-
if (movingLeft &&
|
|
124
|
-
r.startOffset === 1 &&
|
|
125
|
-
textNode.textContent === '\u200B' &&
|
|
126
|
-
isFAIcon(textNode.previousSibling)) {
|
|
127
|
-
event.preventDefault();
|
|
128
|
-
return moveCaret((nr) => nr.setStartBefore(textNode.previousSibling));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (movingLeft && r.startOffset === 0 && isFAIcon(textNode.previousSibling)) {
|
|
132
|
-
event.preventDefault();
|
|
133
|
-
return moveCaret((nr) => nr.setStartBefore(textNode.previousSibling));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (movingLeft &&
|
|
137
|
-
r.startOffset === 0 &&
|
|
138
|
-
isZwsAnchor(textNode.previousSibling) &&
|
|
139
|
-
isFAIcon(textNode.previousSibling.previousSibling)) {
|
|
140
|
-
event.preventDefault();
|
|
141
|
-
return moveCaret((nr) => nr.setStartBefore(textNode.previousSibling.previousSibling));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!movingLeft &&
|
|
145
|
-
r.startOffset === textNode.textContent.length &&
|
|
146
|
-
isFAIcon(textNode.nextSibling)) {
|
|
147
|
-
const icon = textNode.nextSibling;
|
|
148
|
-
const after = icon.nextSibling;
|
|
149
|
-
event.preventDefault();
|
|
150
|
-
if (after?.nodeType === Node.TEXT_NODE) {
|
|
151
|
-
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
152
|
-
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
153
|
-
}
|
|
154
|
-
return moveCaret((nr) => nr.setStartAfter(icon));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!movingLeft &&
|
|
158
|
-
r.startOffset === textNode.textContent.length &&
|
|
159
|
-
isZwsAnchor(textNode.nextSibling) &&
|
|
160
|
-
isFAIcon(textNode.nextSibling.nextSibling)) {
|
|
161
|
-
const icon = textNode.nextSibling.nextSibling;
|
|
162
|
-
const after = icon.nextSibling;
|
|
163
|
-
event.preventDefault();
|
|
164
|
-
if (after?.nodeType === Node.TEXT_NODE) {
|
|
165
|
-
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
166
|
-
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
167
|
-
}
|
|
168
|
-
return moveCaret((nr) => nr.setStartAfter(icon));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (sc.nodeType === Node.ELEMENT_NODE) {
|
|
173
|
-
const el = sc;
|
|
174
|
-
if (movingLeft && r.startOffset > 0) {
|
|
175
|
-
const prev = el.childNodes[r.startOffset - 1];
|
|
176
|
-
if (isFAIcon(prev)) {
|
|
177
|
-
event.preventDefault();
|
|
178
|
-
return moveCaret((nr) => nr.setStartBefore(prev));
|
|
179
|
-
}
|
|
180
|
-
if (isZwsAnchor(prev) && isFAIcon(prev.previousSibling)) {
|
|
181
|
-
event.preventDefault();
|
|
182
|
-
return moveCaret((nr) => nr.setStartBefore(prev.previousSibling));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!movingLeft && r.startOffset < el.childNodes.length) {
|
|
186
|
-
const next = el.childNodes[r.startOffset];
|
|
187
|
-
if (isFAIcon(next)) {
|
|
188
|
-
const after = next.nextSibling;
|
|
189
|
-
event.preventDefault();
|
|
190
|
-
if (after?.nodeType === Node.TEXT_NODE) {
|
|
191
|
-
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
192
|
-
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
193
|
-
}
|
|
194
|
-
return moveCaret((nr) => nr.setStartAfter(next));
|
|
195
|
-
}
|
|
196
|
-
if (isZwsAnchor(next) && isFAIcon(next.nextSibling)) {
|
|
197
|
-
const icon = next.nextSibling;
|
|
198
|
-
const after = icon.nextSibling;
|
|
199
|
-
event.preventDefault();
|
|
200
|
-
if (after?.nodeType === Node.TEXT_NODE) {
|
|
201
|
-
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
202
|
-
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
203
|
-
}
|
|
204
|
-
return moveCaret((nr) => nr.setStartAfter(icon));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// -------------------------------------------------------------------------
|
|
211
|
-
// Tab key — indent / outdent list items, or insert soft tab in code blocks
|
|
212
|
-
// -------------------------------------------------------------------------
|
|
213
|
-
if (isKey(event, key.TAB)) {
|
|
214
|
-
const range = currentRange(editable);
|
|
215
|
-
if (!range) return false;
|
|
216
|
-
|
|
217
|
-
const para = closestPara(range.sc, editable);
|
|
218
|
-
if (para && isLi(para)) {
|
|
219
|
-
event.preventDefault();
|
|
220
|
-
if (event.shiftKey) {
|
|
221
|
-
outdent();
|
|
222
|
-
} else {
|
|
223
|
-
execCommand('indent');
|
|
224
|
-
}
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// In a pre/code block, insert spaces using configured tabSize
|
|
229
|
-
if (para?.nodeName.toUpperCase() === 'PRE') {
|
|
230
|
-
if (event.shiftKey) return false;
|
|
231
|
-
event.preventDefault();
|
|
232
|
-
execCommand('insertText', ' '.repeat(options.tabSize || 4));
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Default: insert * tabSize
|
|
237
|
-
if (options.tabSize) {
|
|
238
|
-
if (event.shiftKey) return false;
|
|
239
|
-
event.preventDefault();
|
|
240
|
-
execCommand('insertText', ' '.repeat(options.tabSize));
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// -------------------------------------------------------------------------
|
|
246
|
-
// Shift+Enter — insert <br> instead of opening a new block element
|
|
247
|
-
// -------------------------------------------------------------------------
|
|
248
|
-
if (isKey(event, key.ENTER) && event.shiftKey) {
|
|
249
|
-
event.preventDefault();
|
|
250
|
-
execCommand('insertLineBreak');
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// -------------------------------------------------------------------------
|
|
255
|
-
// Enter key — keep consistent paragraph insertion
|
|
256
|
-
// -------------------------------------------------------------------------
|
|
257
|
-
if (isKey(event, key.ENTER) && !event.shiftKey) {
|
|
258
|
-
const range = currentRange(editable);
|
|
259
|
-
if (!range) return false;
|
|
260
|
-
|
|
261
|
-
// Hoist sc/el once so all guards below can reuse them.
|
|
262
|
-
const sc = range.sc;
|
|
263
|
-
const el = /** @type {Element|null} */ (sc.nodeType === 3 ? sc.parentElement : sc);
|
|
264
|
-
|
|
265
|
-
// Guard: if the cursor is inside a <i> FA icon element (zero text children,
|
|
266
|
-
// rendered entirely by CSS ::before), pressing Enter would split the block
|
|
267
|
-
// and leave an orphan <i> in the new paragraph — visually an "auto-created
|
|
268
|
-
// icon". Push the cursor to just after the <i> first, then fall through so
|
|
269
|
-
// the browser fires its default Enter at a safe text boundary.
|
|
270
|
-
if (el?.nodeName === 'I' && /\bfa-/.test(el.className || '')) {
|
|
271
|
-
const nr = document.createRange();
|
|
272
|
-
nr.setStartAfter(el);
|
|
273
|
-
nr.collapse(true);
|
|
274
|
-
const selI = globalThis.getSelection();
|
|
275
|
-
if (selI) { selI.removeAllRanges(); selI.addRange(nr); }
|
|
276
|
-
return false; // cursor is now outside <i> — let browser default handle Enter
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Video wrapper — Enter should create a new paragraph after the wrapper,
|
|
280
|
-
// not split the wrapper's container and produce an empty video clone.
|
|
281
|
-
const videoWrapper = el?.closest('.an-video-wrapper');
|
|
282
|
-
if (videoWrapper) {
|
|
283
|
-
event.preventDefault();
|
|
284
|
-
const p = document.createElement('p');
|
|
285
|
-
p.innerHTML = '\u00a0';
|
|
286
|
-
videoWrapper.parentNode.insertBefore(p, videoWrapper.nextSibling);
|
|
287
|
-
const nr = document.createRange();
|
|
288
|
-
nr.setStart(p, 0);
|
|
289
|
-
nr.collapse(true);
|
|
290
|
-
const sel = globalThis.getSelection();
|
|
291
|
-
sel.removeAllRanges();
|
|
292
|
-
sel.addRange(nr);
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Checklist — Enter creates new item; empty item exits the list
|
|
297
|
-
const checkLi = el?.closest('.an-checklist li');
|
|
298
|
-
if (checkLi) {
|
|
299
|
-
event.preventDefault();
|
|
300
|
-
const ul = checkLi.closest('.an-checklist');
|
|
301
|
-
const sel = globalThis.getSelection();
|
|
302
|
-
let nativeRange = sel.getRangeAt(0);
|
|
303
|
-
|
|
304
|
-
// Helper: get trimmed text content of a li, excluding the checkbox INPUT.
|
|
305
|
-
// Strip both \u00a0 (placeholder nbsp) and \u200B (ZWS cursor anchors).
|
|
306
|
-
const liText = (li) =>
|
|
307
|
-
Array.from(li.childNodes)
|
|
308
|
-
.filter((n) => !(n.nodeType === 1 && n.tagName === 'INPUT'))
|
|
309
|
-
.map((n) => n.textContent).join('').replace(/[\u00a0\u200B]/g, ' ').trim();
|
|
310
|
-
|
|
311
|
-
// 1. Check if the ENTIRE item is empty BEFORE any DOM mutation.
|
|
312
|
-
// (Do NOT check only the "before-cursor" part — that check incorrectly
|
|
313
|
-
// exits the list when cursor is at the start of a non-empty item.)
|
|
314
|
-
if (!liText(checkLi)) {
|
|
315
|
-
// Empty item — exit checklist, insert <p> after list
|
|
316
|
-
const p = document.createElement('p');
|
|
317
|
-
p.innerHTML = '\u00a0';
|
|
318
|
-
ul.parentNode.insertBefore(p, ul.nextSibling);
|
|
319
|
-
checkLi.remove();
|
|
320
|
-
if (ul.children.length === 0) ul.remove();
|
|
321
|
-
const nr = document.createRange();
|
|
322
|
-
nr.setStart(p.firstChild, 0);
|
|
323
|
-
nr.collapse(true);
|
|
324
|
-
sel.removeAllRanges();
|
|
325
|
-
sel.addRange(nr);
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 2. If selection is not collapsed, delete the selected content first —
|
|
330
|
-
// mirrors browser-default Enter behaviour (delete selection, then split).
|
|
331
|
-
if (!nativeRange.collapsed) {
|
|
332
|
-
nativeRange.deleteContents();
|
|
333
|
-
// nativeRange is now collapsed at the deletion point; re-read it
|
|
334
|
-
if (sel.rangeCount === 0 || !checkLi.isConnected) return true;
|
|
335
|
-
nativeRange = sel.getRangeAt(0);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// 3. Extract everything from cursor to end of li into afterFrag.
|
|
339
|
-
// Use startContainer/startOffset (cursor position after potential delete),
|
|
340
|
-
// NOT endContainer/endOffset which is wrong for non-collapsed ranges.
|
|
341
|
-
const afterFrag = extractAfterContent(nativeRange, checkLi);
|
|
342
|
-
|
|
343
|
-
// 4. Build the new checklist item with the extracted "after" content.
|
|
344
|
-
const newLi = document.createElement('li');
|
|
345
|
-
const cb = document.createElement('input');
|
|
346
|
-
cb.type = 'checkbox';
|
|
347
|
-
cb.setAttribute('contenteditable', 'false');
|
|
348
|
-
newLi.appendChild(cb);
|
|
349
|
-
|
|
350
|
-
// Append extracted "after" content (if any) then insert the new item.
|
|
351
|
-
if (afterFrag.textContent.replace(/[\u00a0\u200B]/g, '').length > 0) {
|
|
352
|
-
newLi.appendChild(afterFrag);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Always ensure a text node exists so the cursor has a text-level
|
|
356
|
-
// anchor. Use \u200B (zero-width space) instead of an empty string:
|
|
357
|
-
// Chrome does not reliably honour a Selection in an empty text node and
|
|
358
|
-
// may normalise it to element-level, placing the caret before the
|
|
359
|
-
// absolutely-positioned checkbox. \u200B is stripped by getHTML().
|
|
360
|
-
let cursorNode = newLi.childNodes[1]; // first child after checkbox
|
|
361
|
-
if (cursorNode?.nodeType !== Node.TEXT_NODE) {
|
|
362
|
-
cursorNode = document.createTextNode('\u200B');
|
|
363
|
-
newLi.appendChild(cursorNode);
|
|
364
|
-
}
|
|
365
|
-
checkLi.after(newLi);
|
|
366
|
-
|
|
367
|
-
const nr = document.createRange();
|
|
368
|
-
nr.setStart(cursorNode, 0);
|
|
369
|
-
nr.collapse(true);
|
|
370
|
-
sel.removeAllRanges();
|
|
371
|
-
sel.addRange(nr);
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const para = closestPara(range.sc, editable);
|
|
376
|
-
|
|
377
|
-
// Enter in a pre/code block: insert a literal newline instead of a new block
|
|
378
|
-
if (para?.nodeName.toUpperCase() === 'PRE') {
|
|
379
|
-
event.preventDefault();
|
|
380
|
-
execCommand('insertText', '\n');
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Pressing Enter at the end of a blockquote should exit it
|
|
385
|
-
if (para?.nodeName.toUpperCase() === 'BLOCKQUOTE') {
|
|
386
|
-
const native = range.toNativeRange();
|
|
387
|
-
native.setEnd(para, para.childNodes.length);
|
|
388
|
-
if (native.toString() === '' && range.isCollapsed()) {
|
|
389
|
-
event.preventDefault();
|
|
390
|
-
execCommand('formatBlock', '<p>');
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return false;
|
|
397
|
-
}
|
package/src/js/index.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* index.js - Public entry point for AutumnNote
|
|
3
|
-
*
|
|
4
|
-
* Usage (module):
|
|
5
|
-
* import AutumnNote from 'autumnnote';
|
|
6
|
-
* const editor = AutumnNote.create('#my-editor', { placeholder: 'Start typing…' });
|
|
7
|
-
*
|
|
8
|
-
* Usage (UMD / script tag):
|
|
9
|
-
* const editor = AutumnNote.create('#my-editor');
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// @ts-ignore
|
|
13
|
-
import '../styles/autumnnote.scss';
|
|
14
|
-
import { Context, _customModules, _globalPlugins } from './Context.js';
|
|
15
|
-
import { registerButton, buttons } from './module/Buttons.js';
|
|
16
|
-
import { defaultOptions } from './settings.js';
|
|
17
|
-
import { registerLocale } from './i18n/index.js';
|
|
18
|
-
|
|
19
|
-
// Snapshot of factory defaults taken at module-load time (before any setDefaults() calls)
|
|
20
|
-
const _originalDefaults = { ...defaultOptions };
|
|
21
|
-
|
|
22
|
-
// Re-export for tree-shaking / module consumers
|
|
23
|
-
export { Context } from './Context.js';
|
|
24
|
-
export { defaultOptions } from './settings.js';
|
|
25
|
-
export * from './core/dom.js';
|
|
26
|
-
export * from './core/range.js';
|
|
27
|
-
export * from './core/func.js';
|
|
28
|
-
export * from './core/key.js';
|
|
29
|
-
export * from './core/lists.js';
|
|
30
|
-
export * from './core/env.js';
|
|
31
|
-
export * from './core/sanitise.js';
|
|
32
|
-
export * from './module/Buttons.js';
|
|
33
|
-
export { locales, resolveLocale, registerLocale } from './i18n/index.js';
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Main factory
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
/** @type {WeakMap<Element, Context>} */
|
|
40
|
-
const instances = new WeakMap();
|
|
41
|
-
|
|
42
|
-
const AutumnNote = {
|
|
43
|
-
/**
|
|
44
|
-
* Creates (or returns existing) editor instance on one or more elements.
|
|
45
|
-
*
|
|
46
|
-
* @param {string|Element|NodeList|Element[]} selector
|
|
47
|
-
* @param {import('./settings.js').AsnOptions} [options]
|
|
48
|
-
* @returns {Context|Context[]} single Context or array of Contexts
|
|
49
|
-
*/
|
|
50
|
-
create(selector, options = {}) {
|
|
51
|
-
const elements = resolveElements(selector);
|
|
52
|
-
const ctxs = elements.map((el) => {
|
|
53
|
-
if (instances.has(el)) return instances.get(el);
|
|
54
|
-
const ctx = new Context(/** @type {HTMLElement} */ (el), options);
|
|
55
|
-
ctx.initialize();
|
|
56
|
-
instances.set(el, ctx);
|
|
57
|
-
return ctx;
|
|
58
|
-
});
|
|
59
|
-
return ctxs.length === 1 ? ctxs[0] : ctxs;
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Destroys the editor(s) on the given selector.
|
|
64
|
-
* @param {string|Element|NodeList|Element[]} selector
|
|
65
|
-
*/
|
|
66
|
-
destroy(selector) {
|
|
67
|
-
resolveElements(selector).forEach((el) => {
|
|
68
|
-
const ctx = instances.get(el);
|
|
69
|
-
if (ctx) {
|
|
70
|
-
ctx.destroy();
|
|
71
|
-
instances.delete(el);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Returns the Context instance for a given element (or null).
|
|
78
|
-
* @param {string|Element} selector
|
|
79
|
-
* @returns {Context|null}
|
|
80
|
-
*/
|
|
81
|
-
getInstance(selector) {
|
|
82
|
-
const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
83
|
-
return el ? instances.get(el) || null : null;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
/** Returns a shallow copy of the default options (read-only snapshot). */
|
|
87
|
-
get defaults() { return { ...defaultOptions }; },
|
|
88
|
-
|
|
89
|
-
/** Merges properties into the global defaults, applied to all future instances. */
|
|
90
|
-
setDefaults(overrides) { Object.assign(defaultOptions, overrides); },
|
|
91
|
-
|
|
92
|
-
/** Restores global defaults to their original factory values. */
|
|
93
|
-
resetDefaults() {
|
|
94
|
-
Object.keys(defaultOptions).forEach((k) => delete defaultOptions[k]);
|
|
95
|
-
Object.assign(defaultOptions, _originalDefaults);
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Registers a custom module to be included in every new editor instance.
|
|
100
|
-
* @param {string} name - unique module key used for ctx.invoke() calls
|
|
101
|
-
* @param {Function} ModuleClass - class with initialize() and optional destroy()
|
|
102
|
-
*/
|
|
103
|
-
registerModule(name, ModuleClass) { _customModules.set(name, ModuleClass); },
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Installs a plugin globally — applied to every future editor instance.
|
|
107
|
-
* Plugin `buttons` are registered to the global button registry immediately
|
|
108
|
-
* so they are available when Toolbar initialises inside create().
|
|
109
|
-
* Plugin `install()` is called after all built-in modules have initialised.
|
|
110
|
-
* @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
|
|
111
|
-
* @param {object} [options] - Forwarded to plugin.install(context, options)
|
|
112
|
-
* @returns {typeof AutumnNote}
|
|
113
|
-
*/
|
|
114
|
-
use(plugin, options = {}) {
|
|
115
|
-
if (!plugin || typeof plugin.name !== 'string') {
|
|
116
|
-
throw new TypeError('[AutumnNote] AutumnNote.use: plugin must have a string `name` property.');
|
|
117
|
-
}
|
|
118
|
-
if (_globalPlugins.has(plugin.name)) {
|
|
119
|
-
console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
if (Array.isArray(plugin.buttons)) {
|
|
123
|
-
plugin.buttons.forEach((b) => registerButton(b));
|
|
124
|
-
}
|
|
125
|
-
_globalPlugins.set(plugin.name, { plugin, options });
|
|
126
|
-
return this;
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Returns true if a plugin with the given name has been registered globally.
|
|
131
|
-
* @param {string} name
|
|
132
|
-
* @returns {boolean}
|
|
133
|
-
*/
|
|
134
|
-
hasPlugin(name) { return _globalPlugins.has(name); },
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Registers a single button definition in the global button registry.
|
|
138
|
-
* After create(), call ctx.invoke('toolbar.rebuild') to render new buttons.
|
|
139
|
-
* @param {object} btnDef - ButtonDef-compatible object with a `name` string
|
|
140
|
-
* @returns {typeof AutumnNote}
|
|
141
|
-
*/
|
|
142
|
-
registerButton(btnDef) { registerButton(btnDef); return this; },
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Registers a locale so `lang: '<code>'` can select it.
|
|
146
|
-
* Only English ships in the ESM bundle — import others from
|
|
147
|
-
* `autumnnote/i18n/<code>` and register them here.
|
|
148
|
-
* @param {string} code
|
|
149
|
-
* @param {object} locale
|
|
150
|
-
*/
|
|
151
|
-
registerLocale(code, locale) { registerLocale(code, locale); return this; },
|
|
152
|
-
|
|
153
|
-
/** Registers a slash-menu command for future editor instances. */
|
|
154
|
-
registerSlashCommand(command) {
|
|
155
|
-
if (!command?.id || typeof command.run !== 'function') {
|
|
156
|
-
throw new TypeError('[AutumnNote] Slash command requires an id and run(context) function.');
|
|
157
|
-
}
|
|
158
|
-
const commands = defaultOptions.slashCommands;
|
|
159
|
-
const index = commands.findIndex((item) => item.id === command.id);
|
|
160
|
-
if (index >= 0) commands[index] = command;
|
|
161
|
-
else commands.push(command);
|
|
162
|
-
return this;
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
/** All pre-built button definitions — accessible in every module format including UMD/CJS. */
|
|
166
|
-
buttons,
|
|
167
|
-
|
|
168
|
-
/** Library version */
|
|
169
|
-
version: '2.0.0',
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Helper
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* @param {string|Element|NodeList|Element[]} selector
|
|
178
|
-
* @returns {Element[]}
|
|
179
|
-
*/
|
|
180
|
-
function resolveElements(selector) {
|
|
181
|
-
if (typeof selector === 'string') {
|
|
182
|
-
return Array.from(document.querySelectorAll(selector));
|
|
183
|
-
}
|
|
184
|
-
if (selector instanceof Element) {
|
|
185
|
-
return [selector];
|
|
186
|
-
}
|
|
187
|
-
if (selector instanceof NodeList || Array.isArray(selector)) {
|
|
188
|
-
return /** @type {Element[]} */ (Array.from(selector));
|
|
189
|
-
}
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export default AutumnNote;
|