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.
- package/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.es.js +385 -619
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +12 -3
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -38
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -304
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -322
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1392
- package/src/js/module/Toolbar.js +0 -855
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/module/table-grid.js +0 -102
- package/src/js/module/table-icons.js +0 -33
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2877
|
@@ -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;
|