autumnnote 2.1.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 (65) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.es.js +385 -619
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +20 -20
  6. package/dist/autumnnote.umd.js +20 -20
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
@@ -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
- }