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.
Files changed (64) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +661 -798
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +20 -20
  7. package/dist/autumnnote.umd.js +20 -20
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/icon-data-V0Xqv-wX.js +255 -0
  10. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  11. package/package.json +13 -4
  12. package/types/index.d.ts +8 -0
  13. package/src/js/Context.js +0 -854
  14. package/src/js/core/detectLang.js +0 -98
  15. package/src/js/core/dom.js +0 -372
  16. package/src/js/core/env.js +0 -25
  17. package/src/js/core/key.js +0 -66
  18. package/src/js/core/lists.js +0 -121
  19. package/src/js/core/markdown.js +0 -695
  20. package/src/js/core/range.js +0 -194
  21. package/src/js/core/sanitise.js +0 -231
  22. package/src/js/editing/History.js +0 -266
  23. package/src/js/editing/Style.js +0 -812
  24. package/src/js/editing/Table.js +0 -105
  25. package/src/js/editing/Typing.js +0 -397
  26. package/src/js/index.js +0 -193
  27. package/src/js/index.umd.js +0 -17
  28. package/src/js/module/AutoSaveRestore.js +0 -125
  29. package/src/js/module/BaseDialog.js +0 -133
  30. package/src/js/module/BaseMediaTooltip.js +0 -142
  31. package/src/js/module/BaseResizer.js +0 -312
  32. package/src/js/module/BubbleToolbar.js +0 -483
  33. package/src/js/module/Buttons.js +0 -399
  34. package/src/js/module/Clipboard.js +0 -579
  35. package/src/js/module/CodeTooltip.js +0 -493
  36. package/src/js/module/Codeview.js +0 -125
  37. package/src/js/module/ContextMenu.js +0 -621
  38. package/src/js/module/Editor.js +0 -747
  39. package/src/js/module/EmojiDialog.js +0 -254
  40. package/src/js/module/FindReplace.js +0 -512
  41. package/src/js/module/Fullscreen.js +0 -80
  42. package/src/js/module/IconDialog.js +0 -618
  43. package/src/js/module/ImageCropOverlay.js +0 -586
  44. package/src/js/module/ImageDialog.js +0 -193
  45. package/src/js/module/ImageResizer.js +0 -42
  46. package/src/js/module/ImageTooltip.js +0 -285
  47. package/src/js/module/LinkDialog.js +0 -145
  48. package/src/js/module/LinkTooltip.js +0 -250
  49. package/src/js/module/MarkdownShortcuts.js +0 -250
  50. package/src/js/module/Mention.js +0 -365
  51. package/src/js/module/Placeholder.js +0 -51
  52. package/src/js/module/ShortcutsDialog.js +0 -111
  53. package/src/js/module/SlashMenu.js +0 -376
  54. package/src/js/module/Statusbar.js +0 -246
  55. package/src/js/module/TableTooltip.js +0 -1521
  56. package/src/js/module/Toolbar.js +0 -750
  57. package/src/js/module/VideoDialog.js +0 -193
  58. package/src/js/module/VideoResizer.js +0 -66
  59. package/src/js/module/VideoTooltip.js +0 -248
  60. package/src/js/module/emoji-data.js +0 -496
  61. package/src/js/renderer.js +0 -120
  62. package/src/js/settings.js +0 -214
  63. package/src/styles/_variables.scss +0 -48
  64. package/src/styles/autumnnote.scss +0 -2866
@@ -1,365 +0,0 @@
1
- /**
2
- * Mention.js — @mention autocomplete support.
3
- *
4
- * Activated when `mention.onSearch` option is provided.
5
- *
6
- * When the user types the trigger character (default `@`) followed by at least
7
- * `mention.minChars` characters, `onSearch(query, callback)` is called.
8
- * The callback receives an array of `{ id, label, avatar? }` items which are
9
- * rendered in a floating dropdown. Selecting an item inserts a non-editable
10
- * mention chip and fires `onInsert` (if provided) to override the default HTML.
11
- *
12
- * Options shape (passed as `mention` option object):
13
- * {
14
- * trigger: '@',
15
- * minChars: 1,
16
- * maxResults: 8,
17
- * debounce: 200,
18
- * onSearch: (query, callback) => void,
19
- * onInsert: (item) => string | null,
20
- * onError: (err: Error) => void,
21
- * mentionClass: 'an-mention',
22
- * allowSpaces: false,
23
- * }
24
- */
25
-
26
- import { on } from '../core/dom.js';
27
-
28
- export class Mention {
29
- /** @param {import('../Context.js').Context} context */
30
- constructor(context) {
31
- this.context = context;
32
-
33
- /** @type {HTMLElement|null} */
34
- this._dropdown = null;
35
- /** @type {string} */
36
- this._query = '';
37
- /** @type {number|null} */
38
- this._debounceTimer = null;
39
- /** @type {number} current highlighted index */
40
- this._activeIndex = -1;
41
- /** @type {Array<{id, label, avatar?}>} */
42
- this._items = [];
43
- /** @type {boolean} */
44
- this._open = false;
45
- /** Position of trigger character in the text node */
46
- this._triggerNode = null;
47
- this._triggerOffset = 0;
48
- /** @type {DOMRect|null} Caret rect captured synchronously during input event */
49
- this._caretRect = null;
50
-
51
- this._disposers = [];
52
- }
53
-
54
- initialize() {
55
- const cfg = this.context.options.mention;
56
- if (!cfg || typeof cfg.onSearch !== 'function') return this;
57
- this._cfg = {
58
- trigger: cfg.trigger || '@',
59
- minChars: cfg.minChars ?? 0,
60
- maxResults: cfg.maxResults ?? 8,
61
- debounce: cfg.debounce ?? 200,
62
- onSearch: cfg.onSearch,
63
- onInsert: cfg.onInsert || null,
64
- onError: cfg.onError || null,
65
- mentionClass: cfg.mentionClass || 'an-mention',
66
- allowSpaces: cfg.allowSpaces || false,
67
- };
68
-
69
- this._buildDropdown();
70
-
71
- const editable = this.context.layoutInfo.editable;
72
- const d1 = on(editable, 'keydown', (e) => this._onKeydown(e));
73
- const d2 = on(editable, 'input', () => this._onInput());
74
- const d3 = on(document, 'click', (e) => this._onDocClick(e));
75
- this._disposers.push(d1, d2, d3);
76
- return this;
77
- }
78
-
79
- destroy() {
80
- clearTimeout(this._debounceTimer);
81
- this._dropdown?.remove();
82
- this._dropdown = null;
83
- this._disposers.forEach((d) => d());
84
- this._disposers = [];
85
- }
86
-
87
- // ---------------------------------------------------------------------------
88
- // Dropdown DOM
89
- // ---------------------------------------------------------------------------
90
-
91
- _buildDropdown() {
92
- const el = document.createElement('div');
93
- el.className = 'an-mention-dropdown';
94
- el.setAttribute('role', 'listbox');
95
-
96
- // Event delegation: single listeners on the container instead of per-item
97
- el.addEventListener('mousedown', (e) => e.preventDefault());
98
- el.addEventListener('click', (e) => {
99
- const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
100
- if (item) this._select(+item.dataset.index);
101
- });
102
- el.addEventListener('mousemove', (e) => {
103
- const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
104
- if (item) this._highlightItem(+item.dataset.index);
105
- });
106
-
107
- document.body.appendChild(el);
108
- this._dropdown = el;
109
- }
110
-
111
- _renderItems(items) {
112
- const dd = this._dropdown;
113
- this._items = items.slice(0, this._cfg.maxResults);
114
- this._activeIndex = this._items.length > 0 ? 0 : -1;
115
-
116
- // Build all items in a DocumentFragment — one batch DOM insertion
117
- const frag = document.createDocumentFragment();
118
- this._items.forEach((item, i) => {
119
- const li = document.createElement('div');
120
- li.className = 'an-mention-item';
121
- li.setAttribute('role', 'option');
122
- li.dataset.index = String(i);
123
- if (item.avatar) {
124
- const img = document.createElement('img');
125
- img.src = item.avatar;
126
- img.className = 'an-mention-avatar';
127
- img.alt = '';
128
- li.appendChild(img);
129
- }
130
- const label = document.createElement('span');
131
- label.textContent = item.label;
132
- li.appendChild(label);
133
- frag.appendChild(li);
134
- });
135
-
136
- dd.innerHTML = ''; // one clear
137
- dd.appendChild(frag); // one batch insert
138
-
139
- this._highlightItem(this._activeIndex);
140
- }
141
-
142
- _highlightItem(index) {
143
- if (!this._dropdown) return;
144
- this._dropdown.querySelectorAll('.an-mention-item').forEach((el, i) => {
145
- el.classList.toggle('an-mention-active', i === index);
146
- });
147
- this._activeIndex = index;
148
- }
149
-
150
- _positionDropdown() {
151
- const dd = this._dropdown;
152
- const rect = this._caretRect;
153
- if (!rect || rect.height === 0) return;
154
-
155
- // Measure while invisible to avoid layout flash
156
- dd.style.visibility = 'hidden';
157
- dd.style.display = 'block';
158
-
159
- const ddh = dd.offsetHeight;
160
- const ddw = dd.offsetWidth;
161
-
162
- // position:fixed — coords are already viewport-relative, no scroll offset needed
163
- let top = rect.bottom + 4;
164
- let left = rect.left;
165
-
166
- if (rect.bottom + ddh + 8 > globalThis.innerHeight) {
167
- top = rect.top - ddh - 4;
168
- }
169
- left = Math.max(8, Math.min(left, globalThis.innerWidth - ddw - 8));
170
-
171
- dd.style.top = `${top}px`;
172
- dd.style.left = `${left}px`;
173
- dd.style.visibility = '';
174
- }
175
-
176
- _showDropdown() {
177
- this._open = true;
178
- this._positionDropdown();
179
- }
180
-
181
- _hideDropdown() {
182
- if (this._dropdown) this._dropdown.style.display = 'none';
183
- this._open = false;
184
- this._items = [];
185
- this._activeIndex = -1;
186
- this._triggerNode = null;
187
- this._caretRect = null;
188
- this._query = '';
189
- }
190
-
191
- /**
192
- * Captures the caret rect synchronously during the input event.
193
- * Must be called while the DOM event is still live — getClientRects() on a
194
- * collapsed range is reliable at this point but often empty inside async callbacks.
195
- */
196
- _captureCaretRect() {
197
- // Prefer a range over the trigger character — it has non-zero width and
198
- // getBoundingClientRect() reliably returns a valid rect.
199
- if (this._triggerNode?.isConnected) {
200
- try {
201
- const r = document.createRange();
202
- const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
203
- r.setStart(this._triggerNode, this._triggerOffset);
204
- r.setEnd(this._triggerNode, end);
205
- const candidate = r.getBoundingClientRect();
206
- if (candidate.height > 0) {
207
- this._caretRect = candidate;
208
- return;
209
- }
210
- } catch (_) { void _; }
211
- }
212
-
213
- // Fallback: use getClientRects() on the current collapsed selection
214
- const sel = globalThis.getSelection();
215
- if (!sel?.rangeCount) return;
216
- const rects = sel.getRangeAt(0).getClientRects();
217
- if (rects.length > 0) {
218
- this._caretRect = rects[rects.length - 1];
219
- }
220
- }
221
-
222
- // ---------------------------------------------------------------------------
223
- // Query detection
224
- // ---------------------------------------------------------------------------
225
-
226
- _getQueryAtCursor() {
227
- const sel = globalThis.getSelection();
228
- if (!sel?.rangeCount) return null;
229
- const range = sel.getRangeAt(0);
230
- if (!range.collapsed) return null;
231
-
232
- const node = range.startContainer;
233
- if (node.nodeType !== Node.TEXT_NODE) return null;
234
-
235
- const text = node.textContent.slice(0, range.startOffset);
236
- const trigger = this._cfg.trigger;
237
-
238
- // Find the last occurrence of trigger in the text before cursor
239
- const triggerIdx = text.lastIndexOf(trigger);
240
- if (triggerIdx === -1) return null;
241
-
242
- const afterTrigger = text.slice(triggerIdx + trigger.length);
243
-
244
- // No spaces allowed unless configured
245
- if (!this._cfg.allowSpaces && /\s/.test(afterTrigger)) return null;
246
-
247
- if (afterTrigger.length < this._cfg.minChars) return null;
248
-
249
- this._triggerNode = node;
250
- this._triggerOffset = triggerIdx;
251
- return afterTrigger;
252
- }
253
-
254
- // ---------------------------------------------------------------------------
255
- // Events
256
- // ---------------------------------------------------------------------------
257
-
258
- _onInput() {
259
- if (!this._cfg) return;
260
- const query = this._getQueryAtCursor();
261
- if (query === null) {
262
- this._hideDropdown();
263
- return;
264
- }
265
-
266
- // Capture caret rect NOW, synchronously while the input event is live.
267
- // Inside a setTimeout/debounce callback the selection may still be valid
268
- // but getClientRects() on a collapsed range often returns empty in that context.
269
- this._captureCaretRect();
270
-
271
- this._query = query;
272
- clearTimeout(this._debounceTimer);
273
- this._debounceTimer = setTimeout(() => {
274
- const cb = (items) => {
275
- if (!Array.isArray(items) || items.length === 0) {
276
- this._hideDropdown();
277
- return;
278
- }
279
- this._renderItems(items);
280
- this._showDropdown();
281
- };
282
- let result;
283
- try {
284
- result = this._cfg.onSearch(this._query, cb);
285
- } catch (err) {
286
- this._hideDropdown();
287
- if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
288
- return;
289
- }
290
- if (result && typeof result.then === 'function') {
291
- result.then(cb).catch((err) => {
292
- this._hideDropdown();
293
- if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
294
- });
295
- }
296
- }, this._cfg.debounce);
297
- }
298
-
299
- _onKeydown(e) {
300
- if (!this._cfg || !this._open) return;
301
-
302
- if (e.key === 'ArrowDown') {
303
- e.preventDefault();
304
- const next = (this._activeIndex + 1) % this._items.length;
305
- this._highlightItem(next);
306
- } else if (e.key === 'ArrowUp') {
307
- e.preventDefault();
308
- const prev = (this._activeIndex - 1 + this._items.length) % this._items.length;
309
- this._highlightItem(prev);
310
- } else if (e.key === 'Enter' || e.key === 'Tab') {
311
- if (this._activeIndex >= 0) {
312
- e.preventDefault();
313
- this._select(this._activeIndex);
314
- }
315
- } else if (e.key === 'Escape') {
316
- this._hideDropdown();
317
- }
318
- }
319
-
320
- _onDocClick(e) {
321
- if (!this._open) return;
322
- if (this._dropdown?.contains(e.target)) return;
323
- this._hideDropdown();
324
- }
325
-
326
- // ---------------------------------------------------------------------------
327
- // Insert
328
- // ---------------------------------------------------------------------------
329
-
330
- _select(index) {
331
- const item = this._items[index];
332
- if (!item) return;
333
-
334
- // Delete the trigger + query text from the DOM
335
- if (this._triggerNode) {
336
- const node = this._triggerNode;
337
- const before = node.textContent.slice(0, this._triggerOffset);
338
- const after = node.textContent.slice(this._triggerOffset + this._cfg.trigger.length + this._query.length);
339
- node.textContent = before + after;
340
-
341
- // Move cursor to where the trigger was
342
- const sel = globalThis.getSelection();
343
- const range = document.createRange();
344
- range.setStart(node, this._triggerOffset);
345
- range.collapse(true);
346
- sel.removeAllRanges();
347
- sel.addRange(range);
348
- }
349
-
350
- // Build the chip HTML
351
- let html;
352
- if (typeof this._cfg.onInsert === 'function') {
353
- html = this._cfg.onInsert(item);
354
- }
355
- if (!html) {
356
- const cls = this._cfg.mentionClass;
357
- html = `<span class="${cls}" data-mention-id="${item.id}" contenteditable="false">@${item.label}</span>`;
358
- }
359
-
360
- // Insert a trailing space so the cursor lands outside the chip
361
- this.context.invoke('editor.insertHTML', html + '&#8203;');
362
- this._hideDropdown();
363
- this.context.triggerEvent('change', this.context.getHTML());
364
- }
365
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Placeholder.js - Shows placeholder text when the editor is empty
3
- * Inspired by Summernote's Placeholder module
4
- */
5
-
6
- import { on } from '../core/dom.js';
7
-
8
- export class Placeholder {
9
- /**
10
- * @param {import('../Context.js').Context} context
11
- */
12
- constructor(context) {
13
- this.context = context;
14
- this.options = context.options;
15
- this._disposers = [];
16
- }
17
-
18
- initialize() {
19
- const editable = this.context.layoutInfo.editable;
20
- const placeholder = this.options.placeholder || '';
21
- if (placeholder) {
22
- editable.dataset.placeholder = placeholder;
23
- }
24
-
25
- const update = () => this._update();
26
- const d1 = on(editable, 'input', update);
27
- const d2 = on(editable, 'focus', update);
28
- const d3 = on(editable, 'blur', update);
29
- this._disposers.push(d1, d2, d3);
30
- this._update();
31
- return this;
32
- }
33
-
34
- destroy() {
35
- this._disposers.forEach((d) => d());
36
- this._disposers = [];
37
- }
38
-
39
- _update() {
40
- const editable = this.context.layoutInfo.editable;
41
- const isFocused = document.activeElement === editable;
42
- // Strip ZWS (\u200B) cursor anchors used by checklist/icon insertion in
43
- // addition to regular whitespace before deciding if the editor is empty.
44
- // Without this, a freshly-created checklist item or icon leaves a ZWS in
45
- // the DOM that causes the placeholder to overlap real content (A-1).
46
- const hasText = editable.textContent.replaceAll('\u200B', '').trim().length > 0;
47
- const isEmpty = !hasText &&
48
- !editable.querySelector('img, table, hr, .an-video-wrapper');
49
- editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
50
- }
51
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * ShortcutsDialog.js - Modal dialog listing all keyboard shortcuts.
3
- * Opened via the toolbar '?' button or Shift+? inside the editor.
4
- */
5
-
6
- import { createElement, on } from '../core/dom.js';
7
- import { BaseDialog } from './BaseDialog.js';
8
-
9
- const SHORTCUTS = [
10
- {
11
- category: 'Text Formatting',
12
- items: [
13
- { keys: 'Ctrl + B', action: 'Bold' },
14
- { keys: 'Ctrl + I', action: 'Italic' },
15
- { keys: 'Ctrl + U', action: 'Underline' },
16
- { keys: 'Ctrl + K', action: 'Insert / edit link' },
17
- ],
18
- },
19
- {
20
- category: 'History',
21
- items: [
22
- { keys: 'Ctrl + Z', action: 'Undo' },
23
- { keys: 'Ctrl + Y / Ctrl + Shift + Z', action: 'Redo' },
24
- ],
25
- },
26
- {
27
- category: 'Selection & Navigation',
28
- items: [
29
- { keys: 'Ctrl + A', action: 'Select all content' },
30
- { keys: 'Tab', action: 'Indent list item / insert spaces' },
31
- { keys: 'Shift + Tab', action: 'Outdent list item' },
32
- ],
33
- },
34
- {
35
- category: 'Clipboard',
36
- items: [
37
- { keys: 'Ctrl + Shift + V', action: 'Paste as plain text' },
38
- ],
39
- },
40
- {
41
- category: 'Find & Replace',
42
- items: [
43
- { keys: 'Ctrl + F', action: 'Find in document' },
44
- { keys: 'Ctrl + H', action: 'Find & Replace' },
45
- ],
46
- },
47
- {
48
- category: 'Editor',
49
- items: [
50
- { keys: 'Ctrl + Shift + /', action: 'Show this keyboard shortcuts dialog' },
51
- ],
52
- },
53
- ];
54
-
55
- const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M6 11h.01M10 11h.01M14 11h.01M18 11h.01M6 15h.01M18 15h.01M10 15h4"/><path d="M7 3l5 4 5-4"/></svg>`;
56
-
57
- export class ShortcutsDialog extends BaseDialog {
58
- // ---------------------------------------------------------------------------
59
- // Public API
60
- // ---------------------------------------------------------------------------
61
-
62
- show() {
63
- this._open();
64
- }
65
-
66
- // ---------------------------------------------------------------------------
67
- // Build dialog
68
- // ---------------------------------------------------------------------------
69
-
70
- _buildDialog() {
71
- const L = this.context.locale.shortcutsDialog;
72
- const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
73
- box.classList.add('an-shortcuts-box');
74
-
75
- // Close button pinned to the right of the header row
76
- const closeBtn = createElement('button', {
77
- type: 'button',
78
- class: 'an-icon-close',
79
- 'aria-label': L.close,
80
- style: 'margin-left:auto',
81
- });
82
- closeBtn.textContent = '×';
83
- this._closeBtn = closeBtn;
84
- this._firstInput = closeBtn;
85
- box.querySelector('.an-dialog-header').appendChild(closeBtn);
86
-
87
- const shortcuts = this.context.locale.shortcutsDialog.shortcuts || SHORTCUTS;
88
- shortcuts.forEach(({ category, items }) => {
89
- const catEl = createElement('div', { class: 'an-shortcuts-cat' });
90
- catEl.textContent = category;
91
- box.appendChild(catEl);
92
-
93
- const table = createElement('div', { class: 'an-shortcuts-table' });
94
- items.forEach(({ keys, action }) => {
95
- const row = createElement('div', { class: 'an-shortcuts-row' });
96
- const keyEl = createElement('span', { class: 'an-shortcuts-key' });
97
- keyEl.textContent = keys;
98
- const actEl = createElement('span', { class: 'an-shortcuts-action' });
99
- actEl.textContent = action;
100
- row.append(keyEl, actEl);
101
- table.appendChild(row);
102
- });
103
- box.appendChild(table);
104
- });
105
-
106
- const d1 = on(closeBtn, 'click', () => this._close());
107
- this._disposers.push(d1);
108
-
109
- return overlay;
110
- }
111
- }