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,621 +0,0 @@
1
- // ContextMenu.js - Right-click context menu for editor actions
2
- import { createElement, on } from '../core/dom.js';
3
- import { sanitiseHTML } from '../core/sanitise.js';
4
-
5
- // SVG icon map — 16×16 Heroicons-style paths
6
- const ICONS = {
7
- undo: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>`,
8
- redo: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/></svg>`,
9
- cut: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="20" r="3"/><circle cx="6" cy="4" r="3"/><line x1="19" y1="5" x2="6" y2="19"/><line x1="19" y1="19" x2="13.5" y2="13.5"/></svg>`,
10
- copy: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
11
- paste: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>`,
12
- bold: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg>`,
13
- italic: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>`,
14
- underline: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg>`,
15
- link: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
16
- image: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`,
17
- video: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`,
18
- // Image format operations
19
- floatLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="8" height="8" rx="1"/><line x1="12" y1="6" x2="22" y2="6"/><line x1="12" y1="9" x2="22" y2="9"/><line x1="12" y1="12" x2="22" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
20
- floatRight: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="4" width="8" height="8" rx="1"/><line x1="2" y1="6" x2="12" y2="6"/><line x1="2" y1="9" x2="12" y2="9"/><line x1="2" y1="12" x2="12" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
21
- floatNone: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="1"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="3" y1="19" x2="17" y2="19"/></svg>`,
22
- originalSize:`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
23
- deleteImg: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
24
- // Video format operations
25
- videoAlign: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`,
26
- deleteVideo: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
27
- // Format painter operations
28
- copyFormat: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
29
- pasteFormat: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/><path d="m9 14 2 2 4-4"/></svg>`,
30
- removeFormat:`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>`,
31
- // Table
32
- table: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>`,
33
- // Color
34
- textColor: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/></svg>`,
35
- highlightColor: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/><line x1="3" y1="21" x2="21" y2="21"/></svg>`,
36
- noColor: `<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="4" y1="4" x2="20" y2="20"/><line x1="20" y1="4" x2="4" y2="20"/></svg>`,
37
- back: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`,
38
- };
39
-
40
- const COLOR_PRESETS = [
41
- // Grayscale
42
- '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
43
- // Saturated
44
- '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
45
- // Pastel
46
- '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
47
- ];
48
-
49
- function makeColorSubItems(colorType) {
50
- const label = colorType === 'foreColor' ? 'Text Color' : 'Highlight Color';
51
- const localeKey = colorType === 'foreColor' ? 'textColor' : 'highlightColor';
52
- return () => [
53
- { back: true, label, localeKey, navigate: () => defaultItems },
54
- { colorPalette: true, colorType },
55
- ];
56
- }
57
-
58
- const defaultItems = [
59
- { name: 'cut', label: 'Cut', icon: ICONS.cut, action: () => document.execCommand('cut') },
60
- { name: 'copy', label: 'Copy', icon: ICONS.copy, action: () => document.execCommand('copy') },
61
- { name: 'paste', label: 'Paste', icon: ICONS.paste, action: (ctx) => {
62
- if (!navigator.clipboard) return;
63
- const editable = ctx.layoutInfo?.editable;
64
- if (!editable) return;
65
- // Prefer read() to get rich HTML; fall back to readText() for plain text
66
- const doInsert = (html, text) => {
67
- editable.focus();
68
- if (html) {
69
- const clean = sanitiseHTML(html);
70
- document.execCommand('insertHTML', false, clean);
71
- } else if (text) {
72
- document.execCommand('insertText', false, text);
73
- }
74
- ctx.invoke('editor.afterCommand');
75
- };
76
- if (typeof navigator.clipboard.read === 'function') {
77
- navigator.clipboard.read().then((items) => {
78
- for (const item of items) {
79
- if (item.types.includes('text/html')) {
80
- item.getType('text/html').then((blob) => blob.text()).then((html) => doInsert(html, null)).catch(() => {});
81
- return;
82
- }
83
- }
84
- // No HTML item — fall through to plain text
85
- navigator.clipboard.readText().then((text) => doInsert(null, text)).catch(() => {});
86
- }).catch(() => {
87
- // read() denied → try readText() as fallback
88
- navigator.clipboard.readText().then((text) => doInsert(null, text)).catch(() => {});
89
- });
90
- } else if (typeof navigator.clipboard.readText === 'function') {
91
- navigator.clipboard.readText().then((text) => doInsert(null, text)).catch(() => {});
92
- }
93
- } },
94
- { separator: true },
95
- { name: 'bold', label: 'Bold', icon: ICONS.bold, action: (ctx) => ctx.invoke('editor.bold') },
96
- { name: 'italic', label: 'Italic', icon: ICONS.italic, action: (ctx) => ctx.invoke('editor.italic') },
97
- { name: 'underline', label: 'Underline', icon: ICONS.underline, action: (ctx) => ctx.invoke('editor.underline') },
98
- { separator: true },
99
- { name: 'textColor', label: 'Text Color', icon: ICONS.textColor, colorStrip: 'foreColor', navigate: makeColorSubItems('foreColor') },
100
- { name: 'highlightColor', label: 'Highlight Color', icon: ICONS.highlightColor, colorStrip: 'hiliteColor', navigate: makeColorSubItems('hiliteColor') },
101
- { separator: true },
102
- { name: 'copyFormat', label: 'Copy Format', icon: ICONS.copyFormat, action: (ctx) => ctx.invoke('contextMenu.copyFormat') },
103
- { name: 'pasteFormat', label: 'Paste Format', icon: ICONS.pasteFormat, action: (ctx) => ctx.invoke('contextMenu.pasteFormat'), disabled: (ctx) => !ctx.invoke('contextMenu.hasCopiedFormat') },
104
- { name: 'removeFormat', label: 'Remove Format', icon: ICONS.removeFormat, action: (ctx) => ctx.invoke('contextMenu.removeFormat') },
105
- { separator: true },
106
- { name: 'link', label: 'Insert Link', icon: ICONS.link, action: (ctx) => ctx.invoke('linkDialog.show') },
107
- { name: 'image', label: 'Insert Image', icon: ICONS.image, action: (ctx) => ctx.invoke('imageDialog.show') },
108
- { name: 'video', label: 'Insert Video', icon: ICONS.video, action: (ctx) => ctx.invoke('videoDialog.show') },
109
- { name: 'table', label: 'Insert Table', icon: ICONS.table, tableGrid: true },
110
- ];
111
-
112
- export class ContextMenu {
113
- /** @param {import('../Context.js').Context} context */
114
- constructor(context) {
115
- this.context = context;
116
- this.options = context.options || {};
117
- this._items = this.options.contextMenu?.items || defaultItems;
118
- this.el = null;
119
- this._disposers = [];
120
- this._menuDisposers = []; // disposers for dynamically-rendered menu buttons
121
- this._lastX = 0;
122
- this._lastY = 0;
123
- this._copiedFormat = null;
124
- this._savedRange = null;
125
- }
126
-
127
- initialize() {
128
- this.el = createElement('div', { class: 'an-contextmenu', role: 'menu', 'aria-hidden': 'true' });
129
- this.el.style.display = 'none';
130
- document.body.appendChild(this.el);
131
-
132
- this._renderItems(this._items);
133
-
134
- const editable = this.context.layoutInfo?.editable;
135
- if (editable) {
136
- this._disposers.push(on(editable, 'contextmenu', (e) => this._onContextMenu(e)));
137
- }
138
-
139
- this._disposers.push(
140
- on(document, 'click', (e) => this._maybeHide(e)),
141
- on(document, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Escape') this.hide(); }),
142
- on(globalThis, 'scroll', () => this.hide(), { passive: true }),
143
- );
144
-
145
- return this;
146
- }
147
-
148
- destroy() {
149
- this._menuDisposers.forEach((d) => { try { d(); } catch (_e) { void _e; } });
150
- this._menuDisposers = [];
151
- this._disposers.forEach((d) => { try { d(); } catch (_e) { void _e; } });
152
- this._disposers = [];
153
- if (this.el) this.el.remove();
154
- this.el = null;
155
- }
156
-
157
- _renderItems(items) {
158
- // Clean up previously registered per-item listeners
159
- this._menuDisposers.forEach((d) => d());
160
- this._menuDisposers = [];
161
-
162
- if (!this.el) return;
163
- this.el.innerHTML = '';
164
-
165
- items.forEach((it) => {
166
- if (it.separator || it.sep) {
167
- this.el.appendChild(createElement('div', { class: 'an-context-sep' }));
168
- return;
169
- }
170
-
171
- // Back-navigation header
172
- if (it.back) {
173
- const backBtn = createElement('button', { type: 'button', class: 'an-context-back' });
174
- const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
175
- iconSpan.innerHTML = ICONS.back;
176
- backBtn.appendChild(iconSpan);
177
- const backLabel = (it.localeKey && this.context.locale.contextMenu[it.localeKey]) || it.label || this.context.locale.contextMenu.back || 'Back';
178
- backBtn.appendChild(createElement('span', { class: 'an-context-label' }, [backLabel]));
179
- const off = on(backBtn, 'click', (e) => {
180
- e.stopPropagation();
181
- const curLeft = Number.parseFloat(this.el.style.left);
182
- const curTop = Number.parseFloat(this.el.style.top);
183
- this._renderItems(it.navigate());
184
- this._reposition(curLeft, curTop);
185
- });
186
- this._menuDisposers.push(off);
187
- this.el.appendChild(backBtn);
188
- return;
189
- }
190
-
191
- // Submenu-navigate item (shows ▶ chevron, re-renders without closing)
192
- if (it.navigate) {
193
- const btn = createElement('button', { type: 'button', class: 'an-context-item an-context-submenu', 'data-name': it.name || '' });
194
- if (it.icon) {
195
- if (it.colorStrip) {
196
- // Toolbar-style icon: SVG stacked above a colored strip
197
- const iconWrap = createElement('span', { class: 'an-context-icon an-context-icon--color', 'aria-hidden': 'true' });
198
- const svgSpan = createElement('span', { class: 'an-context-icon-svg' });
199
- svgSpan.innerHTML = it.icon;
200
- const strip = createElement('span', { class: 'an-context-color-strip' });
201
- strip.style.background = this._getSelectionColor(it.colorStrip);
202
- iconWrap.appendChild(svgSpan);
203
- iconWrap.appendChild(strip);
204
- btn.appendChild(iconWrap);
205
- } else {
206
- const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
207
- iconSpan.innerHTML = it.icon;
208
- btn.appendChild(iconSpan);
209
- }
210
- }
211
- btn.appendChild(createElement('span', { class: 'an-context-label' }, [(this.context.locale.contextMenu[it.name] || it.label || it.name)]));
212
- const chevron = createElement('span', { class: 'an-context-chevron', 'aria-hidden': 'true' });
213
- chevron.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`;
214
- btn.appendChild(chevron);
215
- const off = on(btn, 'click', (e) => {
216
- e.stopPropagation();
217
- const curLeft = Number.parseFloat(this.el.style.left);
218
- const curTop = Number.parseFloat(this.el.style.top);
219
- this._renderItems(it.navigate());
220
- this._reposition(curLeft, curTop);
221
- });
222
- this._menuDisposers.push(off);
223
- this.el.appendChild(btn);
224
- return;
225
- }
226
-
227
- // Color palette item — renders inline color swatches
228
- if (it.colorPalette) {
229
- const palette = createElement('div', { class: 'an-context-color-palette' });
230
- COLOR_PRESETS.forEach((color) => {
231
- const sw = createElement('div', {
232
- class: 'an-context-color-swatch',
233
- title: color,
234
- role: 'button',
235
- 'aria-label': color,
236
- });
237
- sw.style.background = color;
238
- const offSw = on(sw, 'click', (e) => { e.stopPropagation(); this._applyColor(it.colorType, color); });
239
- this._menuDisposers.push(offSw);
240
- palette.appendChild(sw);
241
- });
242
- if (it.colorType === 'hiliteColor') {
243
- const noColor = createElement('div', {
244
- class: 'an-context-color-swatch an-context-color-none',
245
- title: this.context.locale.contextMenu.noHighlight || 'No highlight',
246
- role: 'button',
247
- 'aria-label': this.context.locale.contextMenu.noHighlight || 'No highlight',
248
- });
249
- noColor.innerHTML = ICONS.noColor;
250
- const offNo = on(noColor, 'click', (e) => { e.stopPropagation(); this._applyColor('hiliteColor', 'transparent'); });
251
- this._menuDisposers.push(offNo);
252
- palette.appendChild(noColor);
253
- }
254
- this.el.appendChild(palette);
255
-
256
- // Custom color row
257
- const customRow = createElement('div', { class: 'an-context-color-custom' });
258
- const colorInput = /** @type {HTMLInputElement} */ (createElement('input', {
259
- type: 'color',
260
- value: it.colorType === 'foreColor' ? '#000000' : '#ffff00',
261
- title: this.context.locale.contextMenu.customColor || 'Custom color',
262
- 'aria-label': this.context.locale.contextMenu.customColor || 'Custom color',
263
- }));
264
- const customLabel = createElement('span', {}, [this.context.locale.contextMenu.customColorLabel || 'Custom…']);
265
- const offCustom = on(colorInput, 'change', () => this._applyColor(it.colorType, colorInput.value));
266
- this._menuDisposers.push(offCustom);
267
- customRow.appendChild(colorInput);
268
- customRow.appendChild(customLabel);
269
- this.el.appendChild(customRow);
270
- return;
271
- }
272
-
273
- // Table grid picker item — expands an inline grid panel when clicked
274
- if (it.tableGrid) {
275
- const GRID_ROWS = 8, GRID_COLS = 8;
276
- const wrapper = createElement('div', { class: 'an-context-table-wrap' });
277
-
278
- const headerBtn = createElement('button', { type: 'button', class: 'an-context-item an-context-submenu', 'data-name': it.name || 'table' });
279
- if (it.icon) {
280
- const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
281
- iconSpan.innerHTML = it.icon;
282
- headerBtn.appendChild(iconSpan);
283
- }
284
- headerBtn.appendChild(createElement('span', { class: 'an-context-label' }, [this.context.locale.contextMenu.table || it.label || 'Insert Table']));
285
- const chevron = createElement('span', { class: 'an-context-chevron', 'aria-hidden': 'true' });
286
- chevron.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`;
287
- headerBtn.appendChild(chevron);
288
-
289
- const panel = createElement('div', { class: 'an-context-table-grid-panel' });
290
- panel.style.display = 'none';
291
-
292
- const gridEl = createElement('div', { class: 'an-table-grid' });
293
- gridEl.style.gridTemplateColumns = `repeat(${GRID_COLS}, 16px)`;
294
- const labelEl = createElement('div', { class: 'an-table-label' });
295
- labelEl.textContent = 'Insert Table';
296
-
297
- const cells = [];
298
- for (let r = 1; r <= GRID_ROWS; r++) {
299
- for (let c = 1; c <= GRID_COLS; c++) {
300
- const cell = createElement('div', { class: 'an-table-cell', 'data-row': String(r), 'data-col': String(c) });
301
- cell.style.width = '16px';
302
- cell.style.height = '16px';
303
- cells.push(cell);
304
- gridEl.appendChild(cell);
305
- }
306
- }
307
-
308
- const setHighlight = (rows, cols) => {
309
- cells.forEach((cell) => {
310
- cell.classList.toggle('active', +cell.dataset.row <= rows && +cell.dataset.col <= cols);
311
- });
312
- labelEl.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.contextMenu.table || 'Insert Table');
313
- };
314
-
315
- panel.appendChild(gridEl);
316
- panel.appendChild(labelEl);
317
-
318
- let expanded = false;
319
- const offHeader = on(headerBtn, 'click', (e) => {
320
- e.stopPropagation();
321
- expanded = !expanded;
322
- panel.style.display = expanded ? '' : 'none';
323
- chevron.style.transform = expanded ? 'rotate(90deg)' : '';
324
- this._reposition();
325
- });
326
- this._menuDisposers.push(offHeader);
327
-
328
- const offMove = on(gridEl, 'mousemove', (e) => {
329
- const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-row]'));
330
- if (!cell) return;
331
- setHighlight(+cell.dataset.row, +cell.dataset.col);
332
- });
333
- const offLeave = on(gridEl, 'mouseleave', () => setHighlight(0, 0));
334
- const offClick = on(gridEl, 'click', (e) => {
335
- const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-row]'));
336
- if (!cell) return;
337
- const rows = +cell.dataset.row;
338
- const cols = +cell.dataset.col;
339
- const editable = this.context.layoutInfo?.editable;
340
- if (editable && this._savedRange) {
341
- editable.focus();
342
- const sel = globalThis.getSelection();
343
- sel.removeAllRanges();
344
- sel.addRange(this._savedRange.cloneRange());
345
- }
346
- this.hide();
347
- this.context.invoke('editor.insertTable', cols, rows);
348
- });
349
- this._menuDisposers.push(offMove, offLeave, offClick);
350
-
351
- wrapper.appendChild(headerBtn);
352
- wrapper.appendChild(panel);
353
- this.el.appendChild(wrapper);
354
- return;
355
- }
356
-
357
- // Regular item
358
- const btn = createElement('button', { type: 'button', class: 'an-context-item', 'data-name': it.name || '' });
359
- if (typeof it.disabled === 'function' ? it.disabled(this.context) : !!it.disabled) /** @type {HTMLButtonElement} */ (btn).disabled = true;
360
- if (it.icon) {
361
- const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
362
- iconSpan.innerHTML = it.icon;
363
- btn.appendChild(iconSpan);
364
- }
365
- btn.appendChild(createElement('span', { class: 'an-context-label' }, [(this.context.locale.contextMenu[it.name] || it.label || it.name)]));
366
- const off = on(btn, 'click', (e) => {
367
- e.stopPropagation();
368
- this.hide();
369
- try { it.action(this.context); } catch (err) { console.error(err); }
370
- });
371
- this._menuDisposers.push(off);
372
- this.el.appendChild(btn);
373
- });
374
- }
375
-
376
- _onContextMenu(event) {
377
- const editable = this.context.layoutInfo?.editable;
378
- if (!editable) return;
379
- if (!editable.contains(event.target)) return;
380
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
381
- event.preventDefault();
382
-
383
- const winSel = globalThis.getSelection();
384
- this._savedRange = (winSel?.rangeCount > 0) ? winSel.getRangeAt(0).cloneRange() : null;
385
- this._renderItems(this._items);
386
-
387
- // Open below the selected text so the selection stays visible.
388
- // Only apply when there is a real (non-collapsed) selection — a collapsed range
389
- // (cursor only) also has height > 0, which would misplace the menu relative
390
- // to the actual click point.
391
- const openX = event.clientX;
392
- let openY = event.clientY;
393
- if (this._savedRange && !this._savedRange.collapsed) {
394
- try {
395
- const selRect = this._savedRange.getBoundingClientRect();
396
- if (selRect.width > 0 && selRect.height > 0) {
397
- // Keep X at click position (feels natural); Y just below the selection.
398
- openY = selRect.bottom + 4;
399
- }
400
- } catch (_) { void _; }
401
- }
402
-
403
- this._lastX = openX;
404
- this._lastY = openY;
405
- this.showAt(openX, openY);
406
- }
407
-
408
- _maybeHide(event) {
409
- if (!this.el) return;
410
- if (!this.el.contains(event.target)) this.hide();
411
- }
412
-
413
- showAt(x, y) {
414
- if (!this.el) return;
415
- this.el.style.display = 'block';
416
- this._reposition(x, y);
417
- this.el.setAttribute('aria-hidden', 'false');
418
- this.context.triggerEvent('contextMenu:show');
419
- }
420
-
421
- _reposition(x, y) {
422
- if (!this.el) return;
423
- const rx = x === undefined ? this._lastX : x;
424
- const ry = y === undefined ? this._lastY : y;
425
- const w = this.el.offsetWidth;
426
- const h = this.el.offsetHeight;
427
- let left = rx;
428
- let top = ry;
429
- // Clamp to viewport so the menu never overflows the screen edge
430
- if (left + w > globalThis.innerWidth - 8) left = globalThis.innerWidth - w - 8;
431
- if (left < 8) left = 8;
432
- if (top + h > globalThis.innerHeight - 8) top = globalThis.innerHeight - h - 8;
433
- if (top < 8) top = 8;
434
- this.el.style.left = `${left}px`;
435
- this.el.style.top = `${top}px`;
436
- }
437
-
438
- hide() {
439
- if (!this.el) return;
440
- this.el.style.display = 'none';
441
- this.el.setAttribute('aria-hidden', 'true');
442
- this.context.triggerEvent('contextMenu:hide');
443
- }
444
-
445
- // ---------------------------------------------------------------------------
446
- // Format operations (Copy Format / Paste Format / Remove Format)
447
- // ---------------------------------------------------------------------------
448
-
449
- /** Read the current selection's text or highlight color for the strip.
450
- * @param {'foreColor'|'hiliteColor'} type
451
- * @returns {string} CSS color string
452
- */
453
- _getSelectionColor(type) {
454
- const range = this._savedRange;
455
- if (!range) return type === 'foreColor' ? '#000000' : 'transparent';
456
- let node = range.startContainer;
457
- if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
458
- if (!node) return type === 'foreColor' ? '#000000' : 'transparent';
459
- const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
460
- if (type === 'foreColor') {
461
- return cs.color || '#000000';
462
- }
463
- const bg = cs.backgroundColor;
464
- return (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
465
- }
466
-
467
- /** Restore selection, apply a color command, then hide the menu. */
468
- _applyColor(type, color) {
469
- const editable = this.context.layoutInfo?.editable;
470
- if (!editable || !this._savedRange) return;
471
- editable.focus();
472
- const sel = globalThis.getSelection();
473
- sel.removeAllRanges();
474
- sel.addRange(this._savedRange.cloneRange());
475
- document.execCommand(type, false, color);
476
- this.context.invoke('editor.afterCommand');
477
- this.hide();
478
- }
479
-
480
- /** Returns true if a format has been copied — used to disable Paste Format. */
481
- hasCopiedFormat() { return !!this._copiedFormat; }
482
-
483
- /** Snapshot inline styles at the selection anchor node. */
484
- copyFormat() {
485
- const range = this._savedRange;
486
- if (!range) return;
487
- const editable = this.context.layoutInfo?.editable;
488
- let node = range.startContainer;
489
- if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
490
- if (!node || !editable?.contains(node)) return;
491
-
492
- // Walk up to collect explicitly-set inline properties from the nearest styled ancestor
493
- const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
494
-
495
- // Detect if an explicit font-family / font-size is set on an element (not just inherited)
496
- const explicitFontFamily = this._findExplicitStyle(node, editable, 'fontFamily');
497
- const explicitFontSize = this._findExplicitStyle(node, editable, 'fontSize');
498
-
499
- this._copiedFormat = {
500
- bold: Number.parseInt(cs.fontWeight, 10) >= 700,
501
- italic: cs.fontStyle === 'italic' || cs.fontStyle === 'oblique',
502
- underline: (cs.textDecorationLine || '').includes('underline'),
503
- strikethrough: (cs.textDecorationLine || '').includes('line-through'),
504
- fontFamily: explicitFontFamily,
505
- fontSize: explicitFontSize,
506
- color: this._isDefaultColor(cs.color) ? null : cs.color,
507
- backgroundColor: cs.backgroundColor,
508
- };
509
- }
510
-
511
- /** Walk up the tree looking for a property explicitly set in inline style. Returns null if only inherited. */
512
- _findExplicitStyle(node, boundary, prop) {
513
- let el = node;
514
- while (el && el !== boundary && el !== document.body) {
515
- if (el.style?.[prop]) return el.style[prop];
516
- // also check font element attributes
517
- if (el.nodeName === 'FONT') {
518
- if (prop === 'fontFamily' && el.getAttribute('face')) return el.getAttribute('face');
519
- if (prop === 'fontSize' && el.getAttribute('size')) {
520
- // font size attribute is 1-7; skip these since we need px
521
- return null;
522
- }
523
- }
524
- el = el.parentElement;
525
- }
526
- return null;
527
- }
528
-
529
- /** Checks if a computed color looks like the default browser text color (black). */
530
- _isDefaultColor(color) {
531
- return !color || color === 'rgb(0, 0, 0)' || color === 'rgba(0, 0, 0, 0)' || color === 'transparent';
532
- }
533
-
534
- /** Apply the most-recently copied format to the saved selection. */
535
- pasteFormat() {
536
- if (!this._copiedFormat || !this._savedRange) return;
537
- const fmt = this._copiedFormat;
538
- const editable = this.context.layoutInfo?.editable;
539
- if (!editable) return;
540
-
541
- // CRITICAL: focus the editable FIRST, then restore selection.
542
- // Calling focus() after addRange() can wipe the selection in Chrome/Firefox.
543
- editable.focus();
544
- const sel = globalThis.getSelection();
545
- sel.removeAllRanges();
546
- sel.addRange(this._savedRange.cloneRange());
547
-
548
- // Strip existing inline formatting so we start from a clean state.
549
- // After removeFormat, bold/italic/underline/strikethrough are guaranteed off.
550
- document.execCommand('removeFormat');
551
-
552
- // Apply each format positively — no toggle-check needed after removeFormat.
553
- if (fmt.bold) document.execCommand('bold');
554
- if (fmt.italic) document.execCommand('italic');
555
- if (fmt.underline) document.execCommand('underline');
556
- if (fmt.strikethrough) document.execCommand('strikeThrough');
557
-
558
- if (fmt.color) document.execCommand('foreColor', false, fmt.color);
559
-
560
- const isTransparent = (c) => !c || c === 'rgba(0, 0, 0, 0)' || c === 'transparent';
561
- if (!isTransparent(fmt.backgroundColor)) {
562
- document.execCommand('hiliteColor', false, fmt.backgroundColor);
563
- }
564
-
565
- // Font family — apply BEFORE font-size DOM manipulation so the selection is still intact.
566
- if (fmt.fontFamily) {
567
- document.execCommand('fontName', false, fmt.fontFamily);
568
- }
569
-
570
- // Font size — use a unique data-marker to avoid touching pre-existing font[size="7"] nodes.
571
- if (fmt.fontSize) {
572
- const marker = `fs-${Date.now()}`;
573
- // Snapshot pre-existing font[size="7"] BEFORE execCommand so we don't accidentally
574
- // replace nodes that were already in the document (the comment below was aspirational
575
- // but the original code never actually excluded them — this Set approach is the fix).
576
- const preExisting = new Set(editable.querySelectorAll('font[size="7"]'));
577
- document.execCommand('fontSize', false, '7');
578
- // Mark only the NEWLY created font[size="7"] elements.
579
- editable.querySelectorAll('font[size="7"]').forEach((el) => {
580
- if (!preExisting.has(el)) /** @type {HTMLElement} */ (el).dataset.anTmp = marker;
581
- });
582
- editable.querySelectorAll(`[data-an-tmp="${marker}"]`).forEach((el) => {
583
- const span = document.createElement('span');
584
- span.style.fontSize = fmt.fontSize;
585
- el.parentNode.insertBefore(span, el);
586
- while (el.firstChild) span.appendChild(el.firstChild);
587
- el.remove();
588
- });
589
- }
590
-
591
- this.context.invoke('editor.afterCommand');
592
- }
593
-
594
- /** Strip all inline formatting from the saved selection. */
595
- removeFormat() {
596
- if (!this._savedRange) return;
597
- const editable = this.context.layoutInfo?.editable;
598
- if (!editable) return;
599
-
600
- editable.focus();
601
- const sel = globalThis.getSelection();
602
- sel.removeAllRanges();
603
- sel.addRange(this._savedRange.cloneRange());
604
-
605
- document.execCommand('removeFormat');
606
-
607
- const range = sel.getRangeAt(0);
608
- const root = range.commonAncestorContainer;
609
- const rootEl = root.nodeType === Node.TEXT_NODE ? root.parentElement : root;
610
- const iter = document.createNodeIterator(rootEl, NodeFilter.SHOW_ELEMENT);
611
- let el;
612
- while ((el = iter.nextNode())) {
613
- if (!editable.contains(el) || el === editable) continue;
614
- try { if (range.intersectsNode(el)) /** @type {Element} */ (el).removeAttribute('style'); } catch { /* ignore */ }
615
- }
616
-
617
- this.context.invoke('editor.afterCommand');
618
- }
619
- }
620
-
621
- export default ContextMenu;