autumnnote 1.15.0 → 2.0.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 (51) hide show
  1. package/README.md +53 -2
  2. package/dist/autumnnote.cjs +21 -21
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +1470 -5884
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +61 -0
  7. package/dist/autumnnote.umd.js +21 -21
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/emoji-data-BDQX5kh-.js +2331 -0
  10. package/dist/emoji-data-BDQX5kh-.js.map +1 -0
  11. package/package.json +9 -5
  12. package/src/js/Context.js +216 -17
  13. package/src/js/editing/History.js +38 -6
  14. package/src/js/i18n/all.js +27 -0
  15. package/src/js/i18n/de.js +15 -0
  16. package/src/js/i18n/en.js +15 -0
  17. package/src/js/i18n/es.js +15 -0
  18. package/src/js/i18n/fr.js +15 -0
  19. package/src/js/i18n/index.js +45 -19
  20. package/src/js/i18n/ja.js +15 -0
  21. package/src/js/i18n/ko.js +15 -0
  22. package/src/js/i18n/vi.js +15 -0
  23. package/src/js/i18n/zh.js +15 -0
  24. package/src/js/index.js +24 -2
  25. package/src/js/index.umd.js +5 -0
  26. package/src/js/module/BaseMediaTooltip.js +142 -0
  27. package/src/js/module/BaseResizer.js +312 -0
  28. package/src/js/module/Clipboard.js +20 -6
  29. package/src/js/module/Editor.js +15 -1
  30. package/src/js/module/EmojiDialog.js +41 -494
  31. package/src/js/module/ImageDialog.js +34 -6
  32. package/src/js/module/ImageResizer.js +23 -241
  33. package/src/js/module/ImageTooltip.js +16 -111
  34. package/src/js/module/MarkdownShortcuts.js +2 -2
  35. package/src/js/module/SlashMenu.js +376 -0
  36. package/src/js/module/Statusbar.js +3 -9
  37. package/src/js/module/VideoResizer.js +38 -239
  38. package/src/js/module/VideoTooltip.js +27 -114
  39. package/src/js/module/emoji-data.js +496 -0
  40. package/src/js/settings.js +27 -0
  41. package/src/styles/autumnnote.scss +49 -0
  42. package/types/i18n/all.d.ts +14 -0
  43. package/types/i18n/de.d.ts +4 -0
  44. package/types/i18n/en.d.ts +4 -0
  45. package/types/i18n/es.d.ts +4 -0
  46. package/types/i18n/fr.d.ts +4 -0
  47. package/types/i18n/ja.d.ts +4 -0
  48. package/types/i18n/ko.d.ts +4 -0
  49. package/types/i18n/vi.d.ts +4 -0
  50. package/types/i18n/zh.d.ts +4 -0
  51. package/types/index.d.ts +113 -4
@@ -0,0 +1,376 @@
1
+ /**
2
+ * SlashMenu.js — Notion-style "/" command palette for quick block insertion.
3
+ *
4
+ * Typing "/" as the very first character of an otherwise-empty block opens a
5
+ * filterable list of quick-insert commands (headings, lists, table, image, …).
6
+ * Arrow keys navigate, Enter/Tab selects, Escape or deleting back past the
7
+ * "/" closes it. Disabled via `slashMenu: false`.
8
+ *
9
+ * Positioning follows the same synchronous-caret-rect technique as Mention.js:
10
+ * getBoundingClientRect() on a zero-width Range over the trigger character is
11
+ * reliable while the triggering DOM event is still live, but often returns an
12
+ * empty rect once deferred into a callback — so the rect is captured eagerly
13
+ * in `_onInput`, not lazily when the menu is rendered.
14
+ */
15
+
16
+ import { on } from '../core/dom.js';
17
+
18
+ export class SlashMenu {
19
+ /** @param {import('../Context.js').Context} context */
20
+ constructor(context) {
21
+ this.context = context;
22
+ this.options = context.options;
23
+ this._disposers = [];
24
+
25
+ /** @type {HTMLElement|null} */
26
+ this._menu = null;
27
+ /** @type {Array<{id: string, label: string, keywords: string, run: () => void}>} */
28
+ this._filtered = [];
29
+ this._activeIndex = -1;
30
+ this._open = false;
31
+ this._query = '';
32
+ /** @type {Text|null} */
33
+ this._textNode = null;
34
+ this._triggerOffset = 0;
35
+ /** @type {DOMRect|null} */
36
+ this._caretRect = null;
37
+ }
38
+
39
+ initialize() {
40
+ if (this.options.slashMenu === false) return this;
41
+ const editable = this.context.layoutInfo.editable;
42
+ const d1 = on(editable, 'input', () => this._onInput());
43
+ const d2 = on(editable, 'keydown', (e) => this._onKeydown(e));
44
+ const d3 = on(document, 'click', (e) => this._onDocClick(e));
45
+ this._disposers.push(d1, d2, d3);
46
+ return this;
47
+ }
48
+
49
+ destroy() {
50
+ this._menu?.remove();
51
+ this._menu = null;
52
+ this._disposers.forEach((d) => d());
53
+ this._disposers = [];
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Command list
58
+ // ---------------------------------------------------------------------------
59
+
60
+ _commands() {
61
+ const L = this.context.locale.slashMenu;
62
+ const ctx = this.context;
63
+ const builtIns = [
64
+ { id: 'h1', label: L.heading1, keywords: 'h1 heading title', run: () => ctx.invoke('editor.formatBlock', 'h1') },
65
+ { id: 'h2', label: L.heading2, keywords: 'h2 heading subtitle', run: () => ctx.invoke('editor.formatBlock', 'h2') },
66
+ { id: 'h3', label: L.heading3, keywords: 'h3 heading', run: () => ctx.invoke('editor.formatBlock', 'h3') },
67
+ { id: 'ul', label: L.bulletList, keywords: 'ul bullet list unordered', run: () => ctx.invoke('editor.insertUL') },
68
+ { id: 'ol', label: L.numberedList, keywords: 'ol numbered list ordered', run: () => ctx.invoke('editor.insertOL') },
69
+ { id: 'checklist', label: L.checklist, keywords: 'checklist todo checkbox task', run: () => ctx.invoke('editor.toggleChecklist') },
70
+ { id: 'quote', label: L.blockquote, keywords: 'quote blockquote', run: () => ctx.invoke('editor.formatBlock', 'blockquote') },
71
+ { id: 'code', label: L.codeBlock, keywords: 'code pre block', run: () => ctx.invoke('editor.formatBlock', 'pre') },
72
+ { id: 'hr', label: L.horizontalRule, keywords: 'hr divider rule line', run: () => ctx.invoke('editor.insertHr') },
73
+ { id: 'table', label: L.table, keywords: 'table grid', run: () => ctx.invoke('editor.insertTable', 3, 3) },
74
+ { id: 'image', label: L.image, keywords: 'image picture photo upload', run: () => ctx.invoke('imageDialog.show') },
75
+ ];
76
+ const custom = (this.options.slashCommands || []).map((command) => ({
77
+ id: command.id,
78
+ label: command.label || command.id,
79
+ keywords: command.keywords || '',
80
+ run: () => command.run(ctx),
81
+ }));
82
+ return [...builtIns, ...custom];
83
+ }
84
+
85
+ refresh() {
86
+ if (this._open) this._filterAndRender();
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Menu DOM
91
+ // ---------------------------------------------------------------------------
92
+
93
+ _buildMenu() {
94
+ const el = document.createElement('div');
95
+ el.className = 'an-slash-menu';
96
+ el.setAttribute('role', 'listbox');
97
+ el.style.display = 'none';
98
+
99
+ el.addEventListener('mousedown', (e) => e.preventDefault());
100
+ el.addEventListener('click', (e) => {
101
+ const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-slash-menu-item'));
102
+ if (item) this._select(+item.dataset.index);
103
+ });
104
+ el.addEventListener('mousemove', (e) => {
105
+ const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-slash-menu-item'));
106
+ if (item) this._highlight(+item.dataset.index);
107
+ });
108
+
109
+ document.body.appendChild(el);
110
+ this._menu = el;
111
+ return el;
112
+ }
113
+
114
+ _renderItems() {
115
+ if (!this._menu) this._buildMenu();
116
+ const menu = this._menu;
117
+ const L = this.context.locale.slashMenu;
118
+
119
+ if (this._filtered.length === 0) {
120
+ menu.innerHTML = '';
121
+ const empty = document.createElement('div');
122
+ empty.className = 'an-slash-menu-empty';
123
+ empty.textContent = L.noResults;
124
+ menu.appendChild(empty);
125
+ this._activeIndex = -1;
126
+ return;
127
+ }
128
+
129
+ const frag = document.createDocumentFragment();
130
+ this._filtered.forEach((item, i) => {
131
+ const row = document.createElement('div');
132
+ row.className = 'an-slash-menu-item';
133
+ row.setAttribute('role', 'option');
134
+ row.id = `an-slash-option-${item.id}`;
135
+ row.dataset.index = String(i);
136
+ row.textContent = item.label;
137
+ frag.appendChild(row);
138
+ });
139
+ menu.innerHTML = '';
140
+ menu.appendChild(frag);
141
+ this._highlight(0);
142
+ }
143
+
144
+ _highlight(index) {
145
+ if (!this._menu) return;
146
+ this._menu.querySelectorAll('.an-slash-menu-item').forEach((el, i) => {
147
+ const active = i === index;
148
+ el.classList.toggle('an-slash-menu-active', active);
149
+ el.setAttribute('aria-selected', String(active));
150
+ if (active) this._menu.setAttribute('aria-activedescendant', el.id);
151
+ });
152
+ this._activeIndex = index;
153
+ }
154
+
155
+ _position() {
156
+ const menu = this._menu;
157
+ const rect = this._caretRect;
158
+ if (!menu || !rect || rect.height === 0) return;
159
+
160
+ menu.style.visibility = 'hidden';
161
+ menu.style.display = 'block';
162
+ const mh = menu.offsetHeight;
163
+ const mw = menu.offsetWidth;
164
+
165
+ let top = rect.bottom + 4;
166
+ let left = rect.left;
167
+ if (rect.bottom + mh + 8 > globalThis.innerHeight) {
168
+ top = rect.top - mh - 4;
169
+ }
170
+ left = Math.max(8, Math.min(left, globalThis.innerWidth - mw - 8));
171
+
172
+ menu.style.top = `${top}px`;
173
+ menu.style.left = `${left}px`;
174
+ menu.style.visibility = '';
175
+ }
176
+
177
+ _open_() {
178
+ this._open = true;
179
+ this._filterAndRender();
180
+ this._position();
181
+ }
182
+
183
+ _close() {
184
+ if (this._menu) this._menu.style.display = 'none';
185
+ this._open = false;
186
+ this._filtered = [];
187
+ this._activeIndex = -1;
188
+ this._menu?.removeAttribute('aria-activedescendant');
189
+ this._textNode = null;
190
+ this._caretRect = null;
191
+ this._query = '';
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Trigger detection
196
+ // ---------------------------------------------------------------------------
197
+
198
+ _isBlock(node) {
199
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
200
+ const display = globalThis.getComputedStyle(node).display;
201
+ return display === 'block' || display === 'list-item' || display === 'table-cell';
202
+ }
203
+
204
+ /**
205
+ * Detects a "/" trigger at the caret, requiring it to be the only content
206
+ * typed so far in its block (i.e. the block's text is exactly "/" + query,
207
+ * nothing before or after). This deliberately avoids firing mid-sentence
208
+ * (e.g. "10/20", "and/or").
209
+ * @returns {{ query: string, textNode: Text, triggerOffset: number } | null}
210
+ */
211
+ _getTriggerContext() {
212
+ const sel = globalThis.getSelection();
213
+ if (!sel?.rangeCount) return null;
214
+ const range = sel.getRangeAt(0);
215
+ if (!range.collapsed) return null;
216
+
217
+ const editable = this.context.layoutInfo.editable;
218
+ if (!editable.contains(range.startContainer)) return null;
219
+ if (range.startContainer.nodeType !== Node.TEXT_NODE) return null;
220
+
221
+ let block = range.startContainer.parentNode;
222
+ while (block && block !== editable && !this._isBlock(block)) block = block.parentNode;
223
+ if (!block || block === editable) return null;
224
+
225
+ const fullText = block.textContent || '';
226
+ const beforeRange = document.createRange();
227
+ beforeRange.setStart(block, 0);
228
+ beforeRange.setEnd(range.startContainer, range.startOffset);
229
+ const before = beforeRange.toString();
230
+
231
+ const m = /^\/(\S*)$/.exec(before);
232
+ if (!m) return null;
233
+ if (fullText.length > before.length) return null; // content after the caret
234
+
235
+ return {
236
+ query: m[1],
237
+ textNode: /** @type {Text} */ (range.startContainer),
238
+ triggerOffset: range.startOffset - m[0].length,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Captures the caret rect synchronously — see file header for why this
244
+ * can't be deferred to a later tick.
245
+ */
246
+ _captureCaretRect(textNode, triggerOffset) {
247
+ try {
248
+ const r = document.createRange();
249
+ const end = Math.min(triggerOffset + 1, textNode.textContent.length);
250
+ r.setStart(textNode, triggerOffset);
251
+ r.setEnd(textNode, end);
252
+ const candidate = r.getBoundingClientRect();
253
+ if (candidate.height > 0) return candidate;
254
+ } catch (_) { void _; }
255
+
256
+ const sel = globalThis.getSelection();
257
+ if (!sel?.rangeCount) return null;
258
+ const rects = sel.getRangeAt(0).getClientRects();
259
+ return rects.length > 0 ? rects[rects.length - 1] : null;
260
+ }
261
+
262
+ _filterAndRender() {
263
+ const q = this._query.toLowerCase();
264
+ this._filtered = q
265
+ ? this._commands().filter((c) => c.keywords.includes(q) || c.label.toLowerCase().includes(q))
266
+ : this._commands();
267
+ this._renderItems();
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Events
272
+ // ---------------------------------------------------------------------------
273
+
274
+ _onInput() {
275
+ const ctx = this._getTriggerContext();
276
+ if (!ctx) {
277
+ if (this._open) this._close();
278
+ return;
279
+ }
280
+
281
+ this._textNode = ctx.textNode;
282
+ this._triggerOffset = ctx.triggerOffset;
283
+ this._query = ctx.query;
284
+ this._caretRect = this._captureCaretRect(ctx.textNode, ctx.triggerOffset);
285
+
286
+ if (!this._open) this._open_();
287
+ else { this._filterAndRender(); this._position(); }
288
+ }
289
+
290
+ _onKeydown(e) {
291
+ if (!this._open) return;
292
+
293
+ if (e.key === 'ArrowDown') {
294
+ e.preventDefault();
295
+ if (this._filtered.length === 0) return;
296
+ this._highlight((this._activeIndex + 1) % this._filtered.length);
297
+ } else if (e.key === 'ArrowUp') {
298
+ e.preventDefault();
299
+ if (this._filtered.length === 0) return;
300
+ this._highlight((this._activeIndex - 1 + this._filtered.length) % this._filtered.length);
301
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
302
+ if (this._activeIndex >= 0) {
303
+ e.preventDefault();
304
+ this._select(this._activeIndex);
305
+ }
306
+ } else if (e.key === 'Escape') {
307
+ e.preventDefault();
308
+ this._close();
309
+ }
310
+ }
311
+
312
+ _onDocClick(e) {
313
+ if (!this._open) return;
314
+ if (this._menu?.contains(e.target)) return;
315
+ this._close();
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Selection
320
+ // ---------------------------------------------------------------------------
321
+
322
+ _deleteTriggerText() {
323
+ const node = this._textNode;
324
+ if (!node?.isConnected) return;
325
+ const before = node.textContent.slice(0, this._triggerOffset);
326
+ const after = node.textContent.slice(this._triggerOffset + 1 + this._query.length);
327
+ node.textContent = before + after;
328
+
329
+ const sel = globalThis.getSelection();
330
+ if (!sel) return;
331
+ const range = document.createRange();
332
+ range.setStart(node, this._triggerOffset);
333
+ range.collapse(true);
334
+ sel.removeAllRanges();
335
+ sel.addRange(range);
336
+ }
337
+
338
+ _makeTriggerRemover() {
339
+ const node = this._textNode;
340
+ const triggerOffset = this._triggerOffset;
341
+ const query = this._query;
342
+ return () => {
343
+ if (!node?.isConnected) return;
344
+ const before = node.textContent.slice(0, triggerOffset);
345
+ const after = node.textContent.slice(triggerOffset + 1 + query.length);
346
+ node.textContent = before + after;
347
+
348
+ const sel = globalThis.getSelection();
349
+ if (!sel) return;
350
+ const range = document.createRange();
351
+ range.setStart(node, triggerOffset);
352
+ range.collapse(true);
353
+ sel.removeAllRanges();
354
+ sel.addRange(range);
355
+ };
356
+ }
357
+
358
+ _select(index) {
359
+ const item = this._filtered[index];
360
+ if (!item) return;
361
+
362
+ if (item.id === 'image') {
363
+ const beforeInsert = this._makeTriggerRemover();
364
+ this._close();
365
+ this.context.invoke('imageDialog.show', { beforeInsert });
366
+ return;
367
+ }
368
+
369
+ this._deleteTriggerText();
370
+ this._close();
371
+ // Editor.* methods invoked by commands already call afterCommand()
372
+ // internally (toolbar/statusbar refresh + debounced undo snapshot + the
373
+ // 'change' event), so no manual triggerEvent here — see Editor.js.
374
+ item.run();
375
+ }
376
+ }
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import { createElement, on } from '../core/dom.js';
7
- import { debounce } from '../core/func.js';
8
7
 
9
8
  // Cache the segmenter instance at module level to avoid per-call allocation
10
9
  const _segmenter =
@@ -97,7 +96,6 @@ export class Statusbar {
97
96
  info.appendChild(this._charCountEl);
98
97
  this.el.appendChild(info);
99
98
 
100
- this._bindContentEvents();
101
99
  this.update();
102
100
  return this;
103
101
  }
@@ -202,13 +200,9 @@ export class Statusbar {
202
200
  // Counter update
203
201
  // ---------------------------------------------------------------------------
204
202
 
205
- _bindContentEvents() {
206
- const editable = this.context.layoutInfo.editable;
207
- const updateDebounced = debounce(() => this.update(), 200);
208
- const d = on(editable, 'input', /** @type {EventListener} */ (updateDebounced));
209
- this._disposers.push(d);
210
- }
211
-
203
+ // Editor.afterCommand() already invokes 'statusbar.update' on every native
204
+ // 'input' event and after every toolbar/formatting command, so a separate
205
+ // content listener here would just re-run this on the same keystroke.
212
206
  update() {
213
207
  if (!this._wordCountEl || !this._charCountEl) return;
214
208
  const editable = this.context.layoutInfo.editable;