autumnnote 1.0.8 → 1.1.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 +401 -628
- package/dist/autumnnote.css +169 -0
- package/dist/autumnnote.es.js +3669 -179
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +3668 -178
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +3 -2
- package/src/js/Context.js +18 -2
- package/src/js/core/func.js +5 -5
- package/src/js/i18n/de.js +320 -0
- package/src/js/i18n/en.js +328 -0
- package/src/js/i18n/es.js +320 -0
- package/src/js/i18n/fr.js +321 -0
- package/src/js/i18n/index.js +59 -0
- package/src/js/i18n/ja.js +321 -0
- package/src/js/i18n/ko.js +320 -0
- package/src/js/i18n/vi.js +321 -0
- package/src/js/i18n/zh.js +321 -0
- package/src/js/index.js +2 -1
- package/src/js/module/AutoSaveRestore.js +126 -0
- package/src/js/module/BubbleToolbar.js +243 -0
- package/src/js/module/CodeTooltip.js +12 -9
- package/src/js/module/ContextMenu.js +41 -37
- package/src/js/module/EmojiDialog.js +8 -7
- package/src/js/module/FindReplace.js +14 -13
- package/src/js/module/IconDialog.js +14 -13
- package/src/js/module/ImageDialog.js +17 -16
- package/src/js/module/ImageTooltip.js +13 -12
- package/src/js/module/LinkDialog.js +10 -9
- package/src/js/module/LinkTooltip.js +6 -5
- package/src/js/module/MarkdownShortcuts.js +253 -0
- package/src/js/module/Mention.js +337 -0
- package/src/js/module/ShortcutsDialog.js +5 -4
- package/src/js/module/Statusbar.js +6 -5
- package/src/js/module/TableTooltip.js +22 -19
- package/src/js/module/Toolbar.js +21 -15
- package/src/js/module/VideoDialog.js +10 -9
- package/src/js/module/VideoTooltip.js +12 -11
- package/src/js/settings.js +23 -0
- package/src/styles/autumnnote.scss +191 -0
- package/types/index.d.ts +114 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BubbleToolbar.js — A mini floating toolbar that appears above the current
|
|
3
|
+
* text selection, allowing quick access to common formatting actions without
|
|
4
|
+
* reaching for the main toolbar.
|
|
5
|
+
*
|
|
6
|
+
* Activated when `bubbleToolbar: true`. The set of visible buttons is
|
|
7
|
+
* controlled by `bubbleToolbarItems` (array of button name strings).
|
|
8
|
+
*
|
|
9
|
+
* Built-in names: 'bold', 'italic', 'underline', 'strikethrough',
|
|
10
|
+
* 'link', 'foreColor', 'removeFormat', 'inlineCode'.
|
|
11
|
+
* Any name not found in the built-in map is silently ignored.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { on } from '../core/dom.js';
|
|
15
|
+
|
|
16
|
+
// Minimal SVG icon set — only what the bubble toolbar needs.
|
|
17
|
+
const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
18
|
+
const _svg = (p) =>
|
|
19
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" ${_S} style="display:block">${p}</svg>`;
|
|
20
|
+
|
|
21
|
+
const _ICONS = {
|
|
22
|
+
bold: _svg('<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"/>'),
|
|
23
|
+
italic: _svg('<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"/>'),
|
|
24
|
+
underline: _svg('<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"/>'),
|
|
25
|
+
strikethrough: _svg('<path d="M17.3 12H6.7"/><path d="M10 6.5C10 5.1 11.1 4 12.5 4c1.4 0 2.5 1.1 2.5 2.5 0 .8-.4 1.5-1 2"/><path d="M14 17.5C14 19 12.9 20 11.5 20 10.1 20 9 18.9 9 17.5c0-.8.4-1.5 1-2"/>'),
|
|
26
|
+
link: _svg('<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"/>'),
|
|
27
|
+
foreColor: _svg('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>'),
|
|
28
|
+
removeFormat: _svg('<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"/>'),
|
|
29
|
+
inlineCode: _svg('<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 3h1a2 2 0 0 1 2 2v5c0 1.1.9 2 2 2a2 2 0 0 1-2 2v5a2 2 0 0 1-2 2h-1"/>'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const _ACTIONS = {
|
|
33
|
+
bold: (ctx) => ctx.invoke('editor.bold'),
|
|
34
|
+
italic: (ctx) => ctx.invoke('editor.italic'),
|
|
35
|
+
underline: (ctx) => ctx.invoke('editor.underline'),
|
|
36
|
+
strikethrough: (ctx) => ctx.invoke('editor.strikethrough'),
|
|
37
|
+
link: (ctx) => ctx.invoke('linkDialog.show'),
|
|
38
|
+
foreColor: (ctx) => {
|
|
39
|
+
const color = window.prompt('Text color (hex or name):', '#000000');
|
|
40
|
+
if (color) ctx.invoke('editor.foreColor', color);
|
|
41
|
+
},
|
|
42
|
+
removeFormat: (ctx) => ctx.invoke('editor.removeFormat'),
|
|
43
|
+
inlineCode: (ctx) => ctx.invoke('editor.inlineCode'),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const _ACTIVE = {
|
|
47
|
+
bold: () => document.queryCommandState('bold'),
|
|
48
|
+
italic: () => document.queryCommandState('italic'),
|
|
49
|
+
underline: () => document.queryCommandState('underline'),
|
|
50
|
+
strikethrough: () => document.queryCommandState('strikeThrough'),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class BubbleToolbar {
|
|
54
|
+
/** @param {import('../Context.js').Context} context */
|
|
55
|
+
constructor(context) {
|
|
56
|
+
this.context = context;
|
|
57
|
+
this.options = context.options;
|
|
58
|
+
|
|
59
|
+
/** @type {HTMLElement|null} */
|
|
60
|
+
this._el = null;
|
|
61
|
+
this._visible = false;
|
|
62
|
+
this._rafId = null;
|
|
63
|
+
this._contextMenuOpen = false;
|
|
64
|
+
this._disposers = [];
|
|
65
|
+
|
|
66
|
+
this._onSelectionChange = this._onSelectionChange.bind(this);
|
|
67
|
+
this._onMousedown = this._onMousedown.bind(this);
|
|
68
|
+
this._onKeydown = this._onKeydown.bind(this);
|
|
69
|
+
this._onContextMenu = this._onContextMenu.bind(this);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
initialize() {
|
|
73
|
+
if (!this.options.bubbleToolbar) return this;
|
|
74
|
+
this._build();
|
|
75
|
+
const d1 = on(document, 'selectionchange', this._onSelectionChange);
|
|
76
|
+
const d2 = on(document, 'mousedown', this._onMousedown);
|
|
77
|
+
const d3 = on(document, 'keydown', this._onKeydown);
|
|
78
|
+
const d4 = on(document, 'contextmenu', this._onContextMenu);
|
|
79
|
+
const d5 = this.context.on('contextMenu:show', () => {
|
|
80
|
+
this._contextMenuOpen = true;
|
|
81
|
+
this._hide();
|
|
82
|
+
});
|
|
83
|
+
const d6 = this.context.on('contextMenu:hide', () => {
|
|
84
|
+
this._contextMenuOpen = false;
|
|
85
|
+
});
|
|
86
|
+
this._disposers.push(d1, d2, d3, d4, d5, d6);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
destroy() {
|
|
91
|
+
if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
|
|
92
|
+
this._el = null;
|
|
93
|
+
this._disposers.forEach((d) => d());
|
|
94
|
+
this._disposers = [];
|
|
95
|
+
cancelAnimationFrame(this._rafId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Build
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
_build() {
|
|
103
|
+
const el = document.createElement('div');
|
|
104
|
+
el.className = 'an-bubble-toolbar';
|
|
105
|
+
el.setAttribute('role', 'toolbar');
|
|
106
|
+
el.setAttribute('aria-label', 'Formatting');
|
|
107
|
+
|
|
108
|
+
const items = this.options.bubbleToolbarItems || ['bold', 'italic', 'underline', 'link', 'foreColor', 'removeFormat'];
|
|
109
|
+
for (const name of items) {
|
|
110
|
+
if (!_ICONS[name] || !_ACTIONS[name]) continue;
|
|
111
|
+
const btn = document.createElement('button');
|
|
112
|
+
btn.type = 'button';
|
|
113
|
+
btn.className = 'an-bubble-btn';
|
|
114
|
+
btn.dataset.name = name;
|
|
115
|
+
btn.setAttribute('aria-label', name);
|
|
116
|
+
btn.innerHTML = _ICONS[name];
|
|
117
|
+
btn.addEventListener('mousedown', (e) => {
|
|
118
|
+
// Prevent mousedown from collapsing selection before click fires
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
});
|
|
121
|
+
btn.addEventListener('click', (e) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
this.context.invoke('editor.focus');
|
|
125
|
+
_ACTIONS[name](this.context);
|
|
126
|
+
this.context.invoke('editor.afterCommand');
|
|
127
|
+
this._syncActive();
|
|
128
|
+
});
|
|
129
|
+
el.appendChild(btn);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
document.body.appendChild(el);
|
|
133
|
+
this._el = el;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Show / hide
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
_show(rect) {
|
|
141
|
+
if (!this._el) return;
|
|
142
|
+
const el = this._el;
|
|
143
|
+
|
|
144
|
+
// Measure while invisible — position:fixed so no scroll offset needed
|
|
145
|
+
el.style.visibility = 'hidden';
|
|
146
|
+
el.style.display = 'flex';
|
|
147
|
+
|
|
148
|
+
const bw = el.offsetWidth;
|
|
149
|
+
const bh = el.offsetHeight;
|
|
150
|
+
const gap = 8;
|
|
151
|
+
|
|
152
|
+
// Center horizontally above the selection; flip below if no room above
|
|
153
|
+
let left = rect.left + rect.width / 2 - bw / 2;
|
|
154
|
+
let top = rect.top - bh - gap;
|
|
155
|
+
|
|
156
|
+
left = Math.max(8, Math.min(left, window.innerWidth - bw - 8));
|
|
157
|
+
|
|
158
|
+
if (top < 8) {
|
|
159
|
+
top = rect.bottom + gap;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
el.style.top = `${top}px`;
|
|
163
|
+
el.style.left = `${left}px`;
|
|
164
|
+
el.style.visibility = '';
|
|
165
|
+
|
|
166
|
+
this._syncActive();
|
|
167
|
+
this._visible = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_hide() {
|
|
171
|
+
if (!this._el) return;
|
|
172
|
+
this._el.style.display = 'none';
|
|
173
|
+
this._visible = false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_syncActive() {
|
|
177
|
+
if (!this._el) return;
|
|
178
|
+
this._el.querySelectorAll('.an-bubble-btn').forEach((btn) => {
|
|
179
|
+
const name = btn.dataset.name;
|
|
180
|
+
const activeFn = _ACTIVE[name];
|
|
181
|
+
btn.classList.toggle('an-active', !!(activeFn && activeFn()));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Events
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
_onSelectionChange() {
|
|
190
|
+
cancelAnimationFrame(this._rafId);
|
|
191
|
+
this._rafId = requestAnimationFrame(() => {
|
|
192
|
+
if (this._contextMenuOpen) return;
|
|
193
|
+
|
|
194
|
+
const sel = window.getSelection();
|
|
195
|
+
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
|
196
|
+
this._hide();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const editable = this.context.layoutInfo.editable;
|
|
201
|
+
if (!editable.contains(sel.anchorNode)) {
|
|
202
|
+
this._hide();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Don't show inside code view or read-only
|
|
207
|
+
if (this.context.options.readOnly) {
|
|
208
|
+
this._hide();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const range = sel.getRangeAt(0);
|
|
213
|
+
const rect = range.getBoundingClientRect();
|
|
214
|
+
if (!rect || rect.width === 0) {
|
|
215
|
+
this._hide();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this._show(rect);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_onMousedown(e) {
|
|
224
|
+
// Hide when clicking outside both the editable and the bubble toolbar
|
|
225
|
+
if (!this._visible) return;
|
|
226
|
+
if (this._el && this._el.contains(e.target)) return;
|
|
227
|
+
const editable = this.context.layoutInfo.editable;
|
|
228
|
+
if (editable.contains(e.target)) return;
|
|
229
|
+
this._hide();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_onKeydown(e) {
|
|
233
|
+
if (e.key === 'Escape' && this._visible) {
|
|
234
|
+
this._hide();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_onContextMenu() {
|
|
239
|
+
// Hide immediately on right-click — contextMenu:show event will also set the flag,
|
|
240
|
+
// but firing here prevents a one-frame flicker before the event propagates.
|
|
241
|
+
this._hide();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -76,16 +76,17 @@ export class CodeTooltip {
|
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
78
|
_buildTooltip() {
|
|
79
|
+
const L = this.context.locale.tooltips.code;
|
|
79
80
|
const el = createElement('div', {
|
|
80
81
|
class: 'an-link-tooltip an-code-tooltip',
|
|
81
82
|
role: 'toolbar',
|
|
82
|
-
'aria-label':
|
|
83
|
+
'aria-label': L.ariaLabel,
|
|
83
84
|
});
|
|
84
85
|
el.style.display = 'none';
|
|
85
86
|
|
|
86
87
|
// Label
|
|
87
88
|
this._label = createElement('span', { class: 'an-link-tooltip-url' });
|
|
88
|
-
this._label.textContent =
|
|
89
|
+
this._label.textContent = L.label;
|
|
89
90
|
el.appendChild(this._label);
|
|
90
91
|
|
|
91
92
|
el.appendChild(this._sep());
|
|
@@ -93,8 +94,8 @@ export class CodeTooltip {
|
|
|
93
94
|
// Language selector
|
|
94
95
|
this._langSelect = createElement('select', {
|
|
95
96
|
class: 'an-code-lang-select',
|
|
96
|
-
title:
|
|
97
|
-
'aria-label':
|
|
97
|
+
title: L.syntaxLanguage,
|
|
98
|
+
'aria-label': L.syntaxAriaLabel,
|
|
98
99
|
});
|
|
99
100
|
const LANGUAGES = [
|
|
100
101
|
['', 'Plain text'], ['javascript', 'JavaScript'], ['typescript', 'TypeScript'],
|
|
@@ -115,25 +116,25 @@ export class CodeTooltip {
|
|
|
115
116
|
el.appendChild(this._sep());
|
|
116
117
|
|
|
117
118
|
// Copy code
|
|
118
|
-
this._copyBtn = this._makeBtn(ICONS.copy,
|
|
119
|
+
this._copyBtn = this._makeBtn(ICONS.copy, L.copyCode, () => this._copyCode());
|
|
119
120
|
|
|
120
121
|
el.appendChild(this._copyBtn);
|
|
121
122
|
|
|
122
123
|
el.appendChild(this._sep());
|
|
123
124
|
|
|
124
125
|
// Toggle word-wrap
|
|
125
|
-
this._wrapBtn = this._makeBtn(ICONS.wrapOn,
|
|
126
|
+
this._wrapBtn = this._makeBtn(ICONS.wrapOn, L.toggleWordWrap, () => this._toggleWrap());
|
|
126
127
|
el.appendChild(this._wrapBtn);
|
|
127
128
|
|
|
128
129
|
el.appendChild(this._sep());
|
|
129
130
|
|
|
130
131
|
// Convert to normal paragraph
|
|
131
|
-
el.appendChild(this._makeBtn(ICONS.toParagraph,
|
|
132
|
+
el.appendChild(this._makeBtn(ICONS.toParagraph, L.convertToParagraph, () => this._toParagraph()));
|
|
132
133
|
|
|
133
134
|
el.appendChild(this._sep());
|
|
134
135
|
|
|
135
136
|
// Delete block
|
|
136
|
-
el.appendChild(this._makeBtn(ICONS.deleteCode,
|
|
137
|
+
el.appendChild(this._makeBtn(ICONS.deleteCode, L.deleteCodeBlock, () => this._delete(), true));
|
|
137
138
|
|
|
138
139
|
// Keep tooltip alive while hovering it
|
|
139
140
|
this._disposers.push(
|
|
@@ -241,7 +242,9 @@ export class CodeTooltip {
|
|
|
241
242
|
const wrapped = (this._activePre.style.whiteSpace || '').includes('pre-wrap')
|
|
242
243
|
|| window.getComputedStyle(this._activePre).whiteSpace === 'pre-wrap';
|
|
243
244
|
this._wrapBtn.classList.toggle('active', wrapped);
|
|
244
|
-
this._wrapBtn.title = wrapped
|
|
245
|
+
this._wrapBtn.title = wrapped
|
|
246
|
+
? this.context.locale.tooltips.code.disableWordWrap
|
|
247
|
+
: this.context.locale.tooltips.code.enableWordWrap;
|
|
245
248
|
}
|
|
246
249
|
|
|
247
250
|
_syncLangSelect() {
|
|
@@ -48,8 +48,9 @@ const COLOR_PRESETS = [
|
|
|
48
48
|
|
|
49
49
|
function makeColorSubItems(colorType) {
|
|
50
50
|
const label = colorType === 'foreColor' ? 'Text Color' : 'Highlight Color';
|
|
51
|
+
const localeKey = colorType === 'foreColor' ? 'textColor' : 'highlightColor';
|
|
51
52
|
return () => [
|
|
52
|
-
{ back: true, label, navigate: () => defaultItems },
|
|
53
|
+
{ back: true, label, localeKey, navigate: () => defaultItems },
|
|
53
54
|
{ colorPalette: true, colorType },
|
|
54
55
|
];
|
|
55
56
|
}
|
|
@@ -171,7 +172,8 @@ export class ContextMenu {
|
|
|
171
172
|
const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
|
|
172
173
|
iconSpan.innerHTML = ICONS.back;
|
|
173
174
|
backBtn.appendChild(iconSpan);
|
|
174
|
-
|
|
175
|
+
const backLabel = (it.localeKey && this.context.locale.contextMenu[it.localeKey]) || it.label || this.context.locale.contextMenu.back || 'Back';
|
|
176
|
+
backBtn.appendChild(createElement('span', { class: 'an-context-label' }, [backLabel]));
|
|
175
177
|
const off = on(backBtn, 'click', (e) => {
|
|
176
178
|
e.stopPropagation();
|
|
177
179
|
const curLeft = parseFloat(this.el.style.left);
|
|
@@ -204,7 +206,7 @@ export class ContextMenu {
|
|
|
204
206
|
btn.appendChild(iconSpan);
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
|
-
btn.appendChild(createElement('span', { class: 'an-context-label' }, [it.label || it.name]));
|
|
209
|
+
btn.appendChild(createElement('span', { class: 'an-context-label' }, [(this.context.locale.contextMenu[it.name] || it.label || it.name)]));
|
|
208
210
|
const chevron = createElement('span', { class: 'an-context-chevron', 'aria-hidden': 'true' });
|
|
209
211
|
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>`;
|
|
210
212
|
btn.appendChild(chevron);
|
|
@@ -238,9 +240,9 @@ export class ContextMenu {
|
|
|
238
240
|
if (it.colorType === 'hiliteColor') {
|
|
239
241
|
const noColor = createElement('div', {
|
|
240
242
|
class: 'an-context-color-swatch an-context-color-none',
|
|
241
|
-
title: 'No highlight',
|
|
243
|
+
title: this.context.locale.contextMenu.noHighlight || 'No highlight',
|
|
242
244
|
role: 'button',
|
|
243
|
-
'aria-label': 'No highlight',
|
|
245
|
+
'aria-label': this.context.locale.contextMenu.noHighlight || 'No highlight',
|
|
244
246
|
});
|
|
245
247
|
noColor.innerHTML = ICONS.noColor;
|
|
246
248
|
const offNo = on(noColor, 'click', (e) => { e.stopPropagation(); this._applyColor('hiliteColor', 'transparent'); });
|
|
@@ -254,10 +256,10 @@ export class ContextMenu {
|
|
|
254
256
|
const colorInput = createElement('input', {
|
|
255
257
|
type: 'color',
|
|
256
258
|
value: it.colorType === 'foreColor' ? '#000000' : '#ffff00',
|
|
257
|
-
title: 'Custom color',
|
|
258
|
-
'aria-label': 'Custom color',
|
|
259
|
+
title: this.context.locale.contextMenu.customColor || 'Custom color',
|
|
260
|
+
'aria-label': this.context.locale.contextMenu.customColor || 'Custom color',
|
|
259
261
|
});
|
|
260
|
-
const customLabel = createElement('span', {}, ['Custom…']);
|
|
262
|
+
const customLabel = createElement('span', {}, [this.context.locale.contextMenu.customColorLabel || 'Custom…']);
|
|
261
263
|
const offCustom = on(colorInput, 'change', () => this._applyColor(it.colorType, colorInput.value));
|
|
262
264
|
this._menuDisposers.push(offCustom);
|
|
263
265
|
customRow.appendChild(colorInput);
|
|
@@ -277,7 +279,7 @@ export class ContextMenu {
|
|
|
277
279
|
iconSpan.innerHTML = it.icon;
|
|
278
280
|
headerBtn.appendChild(iconSpan);
|
|
279
281
|
}
|
|
280
|
-
headerBtn.appendChild(createElement('span', { class: 'an-context-label' }, [it.label || 'Insert Table']));
|
|
282
|
+
headerBtn.appendChild(createElement('span', { class: 'an-context-label' }, [this.context.locale.contextMenu.table || it.label || 'Insert Table']));
|
|
281
283
|
const chevron = createElement('span', { class: 'an-context-chevron', 'aria-hidden': 'true' });
|
|
282
284
|
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>`;
|
|
283
285
|
headerBtn.appendChild(chevron);
|
|
@@ -305,7 +307,7 @@ export class ContextMenu {
|
|
|
305
307
|
cells.forEach((cell) => {
|
|
306
308
|
cell.classList.toggle('active', +cell.dataset.row <= rows && +cell.dataset.col <= cols);
|
|
307
309
|
});
|
|
308
|
-
labelEl.textContent = (rows && cols) ? `${cols} × ${rows}` : 'Insert Table';
|
|
310
|
+
labelEl.textContent = (rows && cols) ? `${cols} × ${rows}` : (this.context.locale.contextMenu.table || 'Insert Table');
|
|
309
311
|
};
|
|
310
312
|
|
|
311
313
|
panel.appendChild(gridEl);
|
|
@@ -358,7 +360,7 @@ export class ContextMenu {
|
|
|
358
360
|
iconSpan.innerHTML = it.icon;
|
|
359
361
|
btn.appendChild(iconSpan);
|
|
360
362
|
}
|
|
361
|
-
btn.appendChild(createElement('span', { class: 'an-context-label' }, [it.label || it.name]));
|
|
363
|
+
btn.appendChild(createElement('span', { class: 'an-context-label' }, [(this.context.locale.contextMenu[it.name] || it.label || it.name)]));
|
|
362
364
|
const off = on(btn, 'click', (e) => {
|
|
363
365
|
e.stopPropagation();
|
|
364
366
|
this.hide();
|
|
@@ -375,12 +377,30 @@ export class ContextMenu {
|
|
|
375
377
|
if (!editable.contains(event.target)) return;
|
|
376
378
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
377
379
|
event.preventDefault();
|
|
378
|
-
|
|
379
|
-
this._lastY = event.clientY;
|
|
380
|
+
|
|
380
381
|
const winSel = window.getSelection();
|
|
381
382
|
this._savedRange = (winSel && winSel.rangeCount > 0) ? winSel.getRangeAt(0).cloneRange() : null;
|
|
382
383
|
this._renderItems(this._items);
|
|
383
|
-
|
|
384
|
+
|
|
385
|
+
// Open below the selected text so the selection stays visible.
|
|
386
|
+
// Only apply when there is a real (non-collapsed) selection — a collapsed range
|
|
387
|
+
// (cursor only) also has height > 0, which would misplace the menu relative
|
|
388
|
+
// to the actual click point.
|
|
389
|
+
let openX = event.clientX;
|
|
390
|
+
let openY = event.clientY;
|
|
391
|
+
if (this._savedRange && !this._savedRange.collapsed) {
|
|
392
|
+
try {
|
|
393
|
+
const selRect = this._savedRange.getBoundingClientRect();
|
|
394
|
+
if (selRect.width > 0 && selRect.height > 0) {
|
|
395
|
+
// Keep X at click position (feels natural); Y just below the selection.
|
|
396
|
+
openY = selRect.bottom + 4;
|
|
397
|
+
}
|
|
398
|
+
} catch (_) {}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this._lastX = openX;
|
|
402
|
+
this._lastY = openY;
|
|
403
|
+
this.showAt(openX, openY);
|
|
384
404
|
}
|
|
385
405
|
|
|
386
406
|
_maybeHide(event) {
|
|
@@ -393,39 +413,22 @@ export class ContextMenu {
|
|
|
393
413
|
this.el.style.display = 'block';
|
|
394
414
|
this._reposition(x, y);
|
|
395
415
|
this.el.setAttribute('aria-hidden', 'false');
|
|
416
|
+
this.context.triggerEvent('contextMenu:show');
|
|
396
417
|
}
|
|
397
418
|
|
|
398
419
|
_reposition(x, y) {
|
|
399
420
|
if (!this.el) return;
|
|
400
421
|
const rx = x !== undefined ? x : this._lastX;
|
|
401
422
|
const ry = y !== undefined ? y : this._lastY;
|
|
402
|
-
const
|
|
423
|
+
const w = this.el.offsetWidth;
|
|
424
|
+
const h = this.el.offsetHeight;
|
|
403
425
|
let left = rx;
|
|
404
426
|
let top = ry;
|
|
405
|
-
|
|
427
|
+
// Clamp to viewport so the menu never overflows the screen edge
|
|
428
|
+
if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8;
|
|
406
429
|
if (left < 8) left = 8;
|
|
407
|
-
if (top +
|
|
430
|
+
if (top + h > window.innerHeight - 8) top = window.innerHeight - h - 8;
|
|
408
431
|
if (top < 8) top = 8;
|
|
409
|
-
|
|
410
|
-
// Avoid covering the saved selection range
|
|
411
|
-
if (this._savedRange) {
|
|
412
|
-
try {
|
|
413
|
-
const sel = this._savedRange.getBoundingClientRect();
|
|
414
|
-
if (sel.width > 0 || sel.height > 0) {
|
|
415
|
-
const overlaps = top < sel.bottom && (top + rect.height) > sel.top &&
|
|
416
|
-
left < sel.right && (left + rect.width) > sel.left;
|
|
417
|
-
if (overlaps) {
|
|
418
|
-
const belowTop = sel.bottom + 6;
|
|
419
|
-
if (belowTop + rect.height <= window.innerHeight - 8) {
|
|
420
|
-
top = belowTop;
|
|
421
|
-
} else {
|
|
422
|
-
top = Math.max(8, sel.top - rect.height - 6);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
} catch (_) { /* stale range — ignore */ }
|
|
427
|
-
}
|
|
428
|
-
|
|
429
432
|
this.el.style.left = `${left}px`;
|
|
430
433
|
this.el.style.top = `${top}px`;
|
|
431
434
|
}
|
|
@@ -434,6 +437,7 @@ export class ContextMenu {
|
|
|
434
437
|
if (!this.el) return;
|
|
435
438
|
this.el.style.display = 'none';
|
|
436
439
|
this.el.setAttribute('aria-hidden', 'true');
|
|
440
|
+
this.context.triggerEvent('contextMenu:hide');
|
|
437
441
|
}
|
|
438
442
|
|
|
439
443
|
// ---------------------------------------------------------------------------
|
|
@@ -555,19 +555,20 @@ export class EmojiDialog {
|
|
|
555
555
|
// ---------------------------------------------------------------------------
|
|
556
556
|
|
|
557
557
|
_buildDialog() {
|
|
558
|
+
const L = this.context.locale.emojiDialog;
|
|
558
559
|
const overlay = createElement('div', {
|
|
559
560
|
class: 'an-dialog-overlay',
|
|
560
561
|
role: 'dialog',
|
|
561
562
|
'aria-modal': 'true',
|
|
562
|
-
'aria-label':
|
|
563
|
+
'aria-label': L.ariaLabel,
|
|
563
564
|
});
|
|
564
565
|
const box = createElement('div', { class: 'an-dialog-box an-emoji-box' });
|
|
565
566
|
|
|
566
567
|
// Title row
|
|
567
568
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
568
569
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
569
|
-
title.textContent =
|
|
570
|
-
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label':
|
|
570
|
+
title.textContent = L.title;
|
|
571
|
+
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
|
|
571
572
|
closeBtn.innerHTML = '×';
|
|
572
573
|
titleRow.append(title, closeBtn);
|
|
573
574
|
|
|
@@ -575,7 +576,7 @@ export class EmojiDialog {
|
|
|
575
576
|
const searchInput = createElement('input', {
|
|
576
577
|
type: 'search',
|
|
577
578
|
class: 'an-input an-icon-search',
|
|
578
|
-
placeholder:
|
|
579
|
+
placeholder: L.searchPlaceholder,
|
|
579
580
|
autocomplete: 'off',
|
|
580
581
|
});
|
|
581
582
|
this._searchInput = searchInput;
|
|
@@ -583,11 +584,11 @@ export class EmojiDialog {
|
|
|
583
584
|
// Category tabs
|
|
584
585
|
const catBar = createElement('div', { class: 'an-icon-cats' });
|
|
585
586
|
const allTab = createElement('button', { type: 'button', class: 'an-icon-cat active', 'data-cat': 'all' });
|
|
586
|
-
allTab.textContent =
|
|
587
|
+
allTab.textContent = L.all;
|
|
587
588
|
catBar.appendChild(allTab);
|
|
588
589
|
EMOJI_CATS.forEach(({ id, label }) => {
|
|
589
590
|
const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
|
|
590
|
-
tab.textContent = label;
|
|
591
|
+
tab.textContent = (L.categories && L.categories[id]) || label;
|
|
591
592
|
catBar.appendChild(tab);
|
|
592
593
|
});
|
|
593
594
|
this._catBar = catBar;
|
|
@@ -611,7 +612,7 @@ export class EmojiDialog {
|
|
|
611
612
|
// Cancel only — clicking an emoji inserts immediately
|
|
612
613
|
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
613
614
|
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
614
|
-
cancelBtn.textContent =
|
|
615
|
+
cancelBtn.textContent = L.cancelBtn;
|
|
615
616
|
btnRow.appendChild(cancelBtn);
|
|
616
617
|
|
|
617
618
|
box.append(titleRow, searchInput, catBar, grid, btnRow);
|
|
@@ -122,7 +122,7 @@ export class FindReplace {
|
|
|
122
122
|
const isReplace = this._mode === 'replace';
|
|
123
123
|
if (replaceRow) replaceRow.style.display = isReplace ? '' : 'none';
|
|
124
124
|
if (replaceActions) replaceActions.style.display = isReplace ? '' : 'none';
|
|
125
|
-
if (title) title.textContent = isReplace ?
|
|
125
|
+
if (title) title.textContent = isReplace ? this.context.locale.findReplace.findReplaceTitle : this.context.locale.findReplace.findTitle;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// ---------------------------------------------------------------------------
|
|
@@ -130,24 +130,25 @@ export class FindReplace {
|
|
|
130
130
|
// ---------------------------------------------------------------------------
|
|
131
131
|
|
|
132
132
|
_buildDialog() {
|
|
133
|
+
const L = this.context.locale.findReplace;
|
|
133
134
|
const overlay = createElement('div', {
|
|
134
135
|
class: 'an-dialog-overlay an-fr-dialog',
|
|
135
136
|
role: 'dialog',
|
|
136
137
|
'aria-modal': 'true',
|
|
137
|
-
'aria-label':
|
|
138
|
+
'aria-label': L.findReplaceTitle,
|
|
138
139
|
});
|
|
139
140
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
140
141
|
|
|
141
142
|
// ---- Title row ----
|
|
142
143
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
143
144
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
144
|
-
title.textContent =
|
|
145
|
+
title.textContent = L.findTitle;
|
|
145
146
|
const closeBtn = createElement('button', {
|
|
146
147
|
type: 'button',
|
|
147
148
|
class: 'an-icon-close',
|
|
148
149
|
'aria-label': 'Close',
|
|
149
150
|
});
|
|
150
|
-
closeBtn.textContent =
|
|
151
|
+
closeBtn.textContent = L.close;
|
|
151
152
|
this._closeBtn = closeBtn;
|
|
152
153
|
titleRow.append(title, closeBtn);
|
|
153
154
|
box.appendChild(titleRow);
|
|
@@ -157,8 +158,8 @@ export class FindReplace {
|
|
|
157
158
|
const findInput = createElement('input', {
|
|
158
159
|
type: 'text',
|
|
159
160
|
class: 'an-input',
|
|
160
|
-
placeholder:
|
|
161
|
-
'aria-label':
|
|
161
|
+
placeholder: L.findPlaceholder,
|
|
162
|
+
'aria-label': L.searchAriaLabel,
|
|
162
163
|
});
|
|
163
164
|
this._findInput = findInput;
|
|
164
165
|
findRow.appendChild(findInput);
|
|
@@ -172,7 +173,7 @@ export class FindReplace {
|
|
|
172
173
|
'aria-label': 'Case sensitive',
|
|
173
174
|
});
|
|
174
175
|
this._caseCheckbox = caseCheckbox;
|
|
175
|
-
caseLabel.append(caseCheckbox, document.createTextNode(
|
|
176
|
+
caseLabel.append(caseCheckbox, document.createTextNode(L.caseSensitive));
|
|
176
177
|
const counter = createElement('span', { class: 'an-fr-counter' });
|
|
177
178
|
this._counterEl = counter;
|
|
178
179
|
optRow.append(caseLabel, counter);
|
|
@@ -181,9 +182,9 @@ export class FindReplace {
|
|
|
181
182
|
// ---- Find actions ----
|
|
182
183
|
const findActions = createElement('div', { class: 'an-dialog-actions an-fr-find-actions' });
|
|
183
184
|
const prevBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
184
|
-
prevBtn.textContent =
|
|
185
|
+
prevBtn.textContent = L.prevBtn;
|
|
185
186
|
const nextBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
186
|
-
nextBtn.textContent =
|
|
187
|
+
nextBtn.textContent = L.nextBtn;
|
|
187
188
|
findActions.append(prevBtn, nextBtn);
|
|
188
189
|
box.appendChild(findActions);
|
|
189
190
|
|
|
@@ -193,8 +194,8 @@ export class FindReplace {
|
|
|
193
194
|
const replaceInput = createElement('input', {
|
|
194
195
|
type: 'text',
|
|
195
196
|
class: 'an-input',
|
|
196
|
-
placeholder:
|
|
197
|
-
'aria-label':
|
|
197
|
+
placeholder: L.replacePlaceholder,
|
|
198
|
+
'aria-label': L.replaceAriaLabel,
|
|
198
199
|
});
|
|
199
200
|
this._replaceInput = replaceInput;
|
|
200
201
|
replaceRow.appendChild(replaceInput);
|
|
@@ -204,9 +205,9 @@ export class FindReplace {
|
|
|
204
205
|
const replaceActions = createElement('div', { class: 'an-dialog-actions an-fr-replace-actions' });
|
|
205
206
|
replaceActions.style.display = 'none';
|
|
206
207
|
const replaceBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
207
|
-
replaceBtn.textContent =
|
|
208
|
+
replaceBtn.textContent = L.replaceBtn;
|
|
208
209
|
const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
209
|
-
replaceAllBtn.textContent =
|
|
210
|
+
replaceAllBtn.textContent = L.replaceAllBtn;
|
|
210
211
|
replaceActions.append(replaceBtn, replaceAllBtn);
|
|
211
212
|
box.appendChild(replaceActions);
|
|
212
213
|
|