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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention.js — @mention autocomplete support.
|
|
3
|
+
*
|
|
4
|
+
* Activated when `mention.onSearch` option is provided.
|
|
5
|
+
*
|
|
6
|
+
* When the user types the trigger character (default `@`) followed by at least
|
|
7
|
+
* `mention.minChars` characters, `onSearch(query, callback)` is called.
|
|
8
|
+
* The callback receives an array of `{ id, label, avatar? }` items which are
|
|
9
|
+
* rendered in a floating dropdown. Selecting an item inserts a non-editable
|
|
10
|
+
* mention chip and fires `onInsert` (if provided) to override the default HTML.
|
|
11
|
+
*
|
|
12
|
+
* Options shape (passed as `mention` option object):
|
|
13
|
+
* {
|
|
14
|
+
* trigger: '@',
|
|
15
|
+
* minChars: 1,
|
|
16
|
+
* maxResults: 8,
|
|
17
|
+
* debounce: 200,
|
|
18
|
+
* onSearch: (query, callback) => void,
|
|
19
|
+
* onInsert: (item) => string | null,
|
|
20
|
+
* mentionClass: 'an-mention',
|
|
21
|
+
* allowSpaces: false,
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { on } from '../core/dom.js';
|
|
26
|
+
|
|
27
|
+
export class Mention {
|
|
28
|
+
/** @param {import('../Context.js').Context} context */
|
|
29
|
+
constructor(context) {
|
|
30
|
+
this.context = context;
|
|
31
|
+
|
|
32
|
+
/** @type {HTMLElement|null} */
|
|
33
|
+
this._dropdown = null;
|
|
34
|
+
/** @type {string} */
|
|
35
|
+
this._query = '';
|
|
36
|
+
/** @type {number|null} */
|
|
37
|
+
this._debounceTimer = null;
|
|
38
|
+
/** @type {number} current highlighted index */
|
|
39
|
+
this._activeIndex = -1;
|
|
40
|
+
/** @type {Array<{id, label, avatar?}>} */
|
|
41
|
+
this._items = [];
|
|
42
|
+
/** @type {boolean} */
|
|
43
|
+
this._open = false;
|
|
44
|
+
/** Position of trigger character in the text node */
|
|
45
|
+
this._triggerNode = null;
|
|
46
|
+
this._triggerOffset = 0;
|
|
47
|
+
/** @type {DOMRect|null} Caret rect captured synchronously during input event */
|
|
48
|
+
this._caretRect = null;
|
|
49
|
+
|
|
50
|
+
this._disposers = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
initialize() {
|
|
54
|
+
const cfg = this.context.options.mention;
|
|
55
|
+
if (!cfg || typeof cfg.onSearch !== 'function') return this;
|
|
56
|
+
this._cfg = {
|
|
57
|
+
trigger: cfg.trigger || '@',
|
|
58
|
+
minChars: cfg.minChars ?? 0,
|
|
59
|
+
maxResults: cfg.maxResults ?? 8,
|
|
60
|
+
debounce: cfg.debounce ?? 200,
|
|
61
|
+
onSearch: cfg.onSearch,
|
|
62
|
+
onInsert: cfg.onInsert || null,
|
|
63
|
+
mentionClass: cfg.mentionClass || 'an-mention',
|
|
64
|
+
allowSpaces: cfg.allowSpaces || false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this._buildDropdown();
|
|
68
|
+
|
|
69
|
+
const editable = this.context.layoutInfo.editable;
|
|
70
|
+
const d1 = on(editable, 'keydown', (e) => this._onKeydown(e));
|
|
71
|
+
const d2 = on(editable, 'input', () => this._onInput());
|
|
72
|
+
const d3 = on(document, 'click', (e) => this._onDocClick(e));
|
|
73
|
+
this._disposers.push(d1, d2, d3);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
destroy() {
|
|
78
|
+
clearTimeout(this._debounceTimer);
|
|
79
|
+
if (this._dropdown && this._dropdown.parentNode) {
|
|
80
|
+
this._dropdown.parentNode.removeChild(this._dropdown);
|
|
81
|
+
}
|
|
82
|
+
this._dropdown = null;
|
|
83
|
+
this._disposers.forEach((d) => d());
|
|
84
|
+
this._disposers = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Dropdown DOM
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
_buildDropdown() {
|
|
92
|
+
const el = document.createElement('div');
|
|
93
|
+
el.className = 'an-mention-dropdown';
|
|
94
|
+
el.setAttribute('role', 'listbox');
|
|
95
|
+
document.body.appendChild(el);
|
|
96
|
+
this._dropdown = el;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_renderItems(items) {
|
|
100
|
+
const dd = this._dropdown;
|
|
101
|
+
dd.innerHTML = '';
|
|
102
|
+
this._items = items.slice(0, this._cfg.maxResults);
|
|
103
|
+
this._activeIndex = this._items.length > 0 ? 0 : -1;
|
|
104
|
+
|
|
105
|
+
this._items.forEach((item, i) => {
|
|
106
|
+
const li = document.createElement('div');
|
|
107
|
+
li.className = 'an-mention-item';
|
|
108
|
+
li.setAttribute('role', 'option');
|
|
109
|
+
li.dataset.index = i;
|
|
110
|
+
if (item.avatar) {
|
|
111
|
+
const img = document.createElement('img');
|
|
112
|
+
img.src = item.avatar;
|
|
113
|
+
img.className = 'an-mention-avatar';
|
|
114
|
+
img.alt = '';
|
|
115
|
+
li.appendChild(img);
|
|
116
|
+
}
|
|
117
|
+
const label = document.createElement('span');
|
|
118
|
+
label.textContent = item.label;
|
|
119
|
+
li.appendChild(label);
|
|
120
|
+
li.addEventListener('mousedown', (e) => e.preventDefault());
|
|
121
|
+
li.addEventListener('click', () => this._select(i));
|
|
122
|
+
dd.appendChild(li);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this._highlightItem(this._activeIndex);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_highlightItem(index) {
|
|
129
|
+
if (!this._dropdown) return;
|
|
130
|
+
this._dropdown.querySelectorAll('.an-mention-item').forEach((el, i) => {
|
|
131
|
+
el.classList.toggle('an-mention-active', i === index);
|
|
132
|
+
});
|
|
133
|
+
this._activeIndex = index;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_positionDropdown() {
|
|
137
|
+
const dd = this._dropdown;
|
|
138
|
+
const rect = this._caretRect;
|
|
139
|
+
if (!rect || rect.height === 0) return;
|
|
140
|
+
|
|
141
|
+
// Measure while invisible to avoid layout flash
|
|
142
|
+
dd.style.visibility = 'hidden';
|
|
143
|
+
dd.style.display = 'block';
|
|
144
|
+
|
|
145
|
+
const ddh = dd.offsetHeight;
|
|
146
|
+
const ddw = dd.offsetWidth;
|
|
147
|
+
|
|
148
|
+
// position:fixed — coords are already viewport-relative, no scroll offset needed
|
|
149
|
+
let top = rect.bottom + 4;
|
|
150
|
+
let left = rect.left;
|
|
151
|
+
|
|
152
|
+
if (rect.bottom + ddh + 8 > window.innerHeight) {
|
|
153
|
+
top = rect.top - ddh - 4;
|
|
154
|
+
}
|
|
155
|
+
left = Math.max(8, Math.min(left, window.innerWidth - ddw - 8));
|
|
156
|
+
|
|
157
|
+
dd.style.top = `${top}px`;
|
|
158
|
+
dd.style.left = `${left}px`;
|
|
159
|
+
dd.style.visibility = '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_showDropdown() {
|
|
163
|
+
this._open = true;
|
|
164
|
+
this._positionDropdown();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_hideDropdown() {
|
|
168
|
+
if (this._dropdown) this._dropdown.style.display = 'none';
|
|
169
|
+
this._open = false;
|
|
170
|
+
this._items = [];
|
|
171
|
+
this._activeIndex = -1;
|
|
172
|
+
this._triggerNode = null;
|
|
173
|
+
this._caretRect = null;
|
|
174
|
+
this._query = '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Captures the caret rect synchronously during the input event.
|
|
179
|
+
* Must be called while the DOM event is still live — getClientRects() on a
|
|
180
|
+
* collapsed range is reliable at this point but often empty inside async callbacks.
|
|
181
|
+
*/
|
|
182
|
+
_captureCaretRect() {
|
|
183
|
+
// Prefer a range over the trigger character — it has non-zero width and
|
|
184
|
+
// getBoundingClientRect() reliably returns a valid rect.
|
|
185
|
+
if (this._triggerNode && this._triggerNode.isConnected) {
|
|
186
|
+
try {
|
|
187
|
+
const r = document.createRange();
|
|
188
|
+
const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
|
|
189
|
+
r.setStart(this._triggerNode, this._triggerOffset);
|
|
190
|
+
r.setEnd(this._triggerNode, end);
|
|
191
|
+
const candidate = r.getBoundingClientRect();
|
|
192
|
+
if (candidate.height > 0) {
|
|
193
|
+
this._caretRect = candidate;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
} catch (_) {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fallback: use getClientRects() on the current collapsed selection
|
|
200
|
+
const sel = window.getSelection();
|
|
201
|
+
if (!sel || !sel.rangeCount) return;
|
|
202
|
+
const rects = sel.getRangeAt(0).getClientRects();
|
|
203
|
+
if (rects.length > 0) {
|
|
204
|
+
this._caretRect = rects[rects.length - 1];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Query detection
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
_getQueryAtCursor() {
|
|
213
|
+
const sel = window.getSelection();
|
|
214
|
+
if (!sel || !sel.rangeCount) return null;
|
|
215
|
+
const range = sel.getRangeAt(0);
|
|
216
|
+
if (!range.collapsed) return null;
|
|
217
|
+
|
|
218
|
+
const node = range.startContainer;
|
|
219
|
+
if (node.nodeType !== Node.TEXT_NODE) return null;
|
|
220
|
+
|
|
221
|
+
const text = node.textContent.slice(0, range.startOffset);
|
|
222
|
+
const trigger = this._cfg.trigger;
|
|
223
|
+
|
|
224
|
+
// Find the last occurrence of trigger in the text before cursor
|
|
225
|
+
const triggerIdx = text.lastIndexOf(trigger);
|
|
226
|
+
if (triggerIdx === -1) return null;
|
|
227
|
+
|
|
228
|
+
const afterTrigger = text.slice(triggerIdx + trigger.length);
|
|
229
|
+
|
|
230
|
+
// No spaces allowed unless configured
|
|
231
|
+
if (!this._cfg.allowSpaces && /\s/.test(afterTrigger)) return null;
|
|
232
|
+
|
|
233
|
+
if (afterTrigger.length < this._cfg.minChars) return null;
|
|
234
|
+
|
|
235
|
+
this._triggerNode = node;
|
|
236
|
+
this._triggerOffset = triggerIdx;
|
|
237
|
+
return afterTrigger;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Events
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
_onInput() {
|
|
245
|
+
if (!this._cfg) return;
|
|
246
|
+
const query = this._getQueryAtCursor();
|
|
247
|
+
if (query === null) {
|
|
248
|
+
this._hideDropdown();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Capture caret rect NOW, synchronously while the input event is live.
|
|
253
|
+
// Inside a setTimeout/debounce callback the selection may still be valid
|
|
254
|
+
// but getClientRects() on a collapsed range often returns empty in that context.
|
|
255
|
+
this._captureCaretRect();
|
|
256
|
+
|
|
257
|
+
this._query = query;
|
|
258
|
+
clearTimeout(this._debounceTimer);
|
|
259
|
+
this._debounceTimer = setTimeout(() => {
|
|
260
|
+
this._cfg.onSearch(this._query, (items) => {
|
|
261
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
262
|
+
this._hideDropdown();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
this._renderItems(items);
|
|
266
|
+
this._showDropdown();
|
|
267
|
+
});
|
|
268
|
+
}, this._cfg.debounce);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_onKeydown(e) {
|
|
272
|
+
if (!this._cfg || !this._open) return;
|
|
273
|
+
|
|
274
|
+
if (e.key === 'ArrowDown') {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
const next = (this._activeIndex + 1) % this._items.length;
|
|
277
|
+
this._highlightItem(next);
|
|
278
|
+
} else if (e.key === 'ArrowUp') {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
const prev = (this._activeIndex - 1 + this._items.length) % this._items.length;
|
|
281
|
+
this._highlightItem(prev);
|
|
282
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
283
|
+
if (this._activeIndex >= 0) {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
this._select(this._activeIndex);
|
|
286
|
+
}
|
|
287
|
+
} else if (e.key === 'Escape') {
|
|
288
|
+
this._hideDropdown();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_onDocClick(e) {
|
|
293
|
+
if (!this._open) return;
|
|
294
|
+
if (this._dropdown && this._dropdown.contains(e.target)) return;
|
|
295
|
+
this._hideDropdown();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Insert
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
_select(index) {
|
|
303
|
+
const item = this._items[index];
|
|
304
|
+
if (!item) return;
|
|
305
|
+
|
|
306
|
+
// Delete the trigger + query text from the DOM
|
|
307
|
+
if (this._triggerNode) {
|
|
308
|
+
const node = this._triggerNode;
|
|
309
|
+
const before = node.textContent.slice(0, this._triggerOffset);
|
|
310
|
+
const after = node.textContent.slice(this._triggerOffset + this._cfg.trigger.length + this._query.length);
|
|
311
|
+
node.textContent = before + after;
|
|
312
|
+
|
|
313
|
+
// Move cursor to where the trigger was
|
|
314
|
+
const sel = window.getSelection();
|
|
315
|
+
const range = document.createRange();
|
|
316
|
+
range.setStart(node, this._triggerOffset);
|
|
317
|
+
range.collapse(true);
|
|
318
|
+
sel.removeAllRanges();
|
|
319
|
+
sel.addRange(range);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Build the chip HTML
|
|
323
|
+
let html;
|
|
324
|
+
if (typeof this._cfg.onInsert === 'function') {
|
|
325
|
+
html = this._cfg.onInsert(item);
|
|
326
|
+
}
|
|
327
|
+
if (!html) {
|
|
328
|
+
const cls = this._cfg.mentionClass;
|
|
329
|
+
html = `<span class="${cls}" data-mention-id="${item.id}" contenteditable="false">@${item.label}</span>`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Insert a trailing space so the cursor lands outside the chip
|
|
333
|
+
this.context.invoke('editor.insertHTML', html + '​');
|
|
334
|
+
this._hideDropdown();
|
|
335
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -93,7 +93,7 @@ export class ShortcutsDialog {
|
|
|
93
93
|
class: 'an-dialog-overlay',
|
|
94
94
|
role: 'dialog',
|
|
95
95
|
'aria-modal': 'true',
|
|
96
|
-
'aria-label':
|
|
96
|
+
'aria-label': this.context.locale.shortcutsDialog.ariaLabel,
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
const box = createElement('div', { class: 'an-dialog-box an-shortcuts-box' });
|
|
@@ -101,18 +101,19 @@ export class ShortcutsDialog {
|
|
|
101
101
|
// Title row with close button
|
|
102
102
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
103
103
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
104
|
-
title.textContent =
|
|
104
|
+
title.textContent = this.context.locale.shortcutsDialog.title;
|
|
105
105
|
const closeBtn = createElement('button', {
|
|
106
106
|
type: 'button',
|
|
107
107
|
class: 'an-icon-close',
|
|
108
|
-
'aria-label':
|
|
108
|
+
'aria-label': this.context.locale.shortcutsDialog.close,
|
|
109
109
|
});
|
|
110
110
|
closeBtn.textContent = '×';
|
|
111
111
|
this._closeBtn = closeBtn;
|
|
112
112
|
titleRow.append(title, closeBtn);
|
|
113
113
|
box.appendChild(titleRow);
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
const shortcuts = this.context.locale.shortcutsDialog.shortcuts || SHORTCUTS;
|
|
116
|
+
shortcuts.forEach(({ category, items }) => {
|
|
116
117
|
const catEl = createElement('div', { class: 'an-shortcuts-cat' });
|
|
117
118
|
catEl.textContent = category;
|
|
118
119
|
box.appendChild(catEl);
|
|
@@ -82,7 +82,7 @@ export class Statusbar {
|
|
|
82
82
|
if (this.options.resizable !== false) {
|
|
83
83
|
const handle = createElement('div', {
|
|
84
84
|
class: 'an-resize-handle',
|
|
85
|
-
title:
|
|
85
|
+
title: this.context.locale.statusbar.resizeHandle,
|
|
86
86
|
'aria-hidden': 'true',
|
|
87
87
|
});
|
|
88
88
|
this._bindResize(handle);
|
|
@@ -220,12 +220,13 @@ export class Statusbar {
|
|
|
220
220
|
const maxWords = this.options.maxWords || 0;
|
|
221
221
|
const maxChars = this.options.maxChars || 0;
|
|
222
222
|
|
|
223
|
+
const LS = this.context.locale.statusbar;
|
|
223
224
|
this._wordCountEl.textContent = maxWords
|
|
224
|
-
?
|
|
225
|
-
:
|
|
225
|
+
? LS.wordsLimit(words, maxWords)
|
|
226
|
+
: LS.words(words);
|
|
226
227
|
this._charCountEl.textContent = maxChars
|
|
227
|
-
?
|
|
228
|
-
:
|
|
228
|
+
? LS.charsLimit(chars, maxChars)
|
|
229
|
+
: LS.chars(chars);
|
|
229
230
|
|
|
230
231
|
// Apply warning / exceeded styles
|
|
231
232
|
_applyLimitClass(this._wordCountEl, words, maxWords);
|
|
@@ -376,55 +376,56 @@ export class TableTooltip {
|
|
|
376
376
|
// ---------------------------------------------------------------------------
|
|
377
377
|
|
|
378
378
|
_buildTooltip() {
|
|
379
|
+
const L = this.context.locale.tooltips.table;
|
|
379
380
|
const el = createElement('div', {
|
|
380
381
|
class: 'an-link-tooltip an-table-tooltip',
|
|
381
382
|
role: 'toolbar',
|
|
382
|
-
'aria-label':
|
|
383
|
+
'aria-label': L.ariaLabel,
|
|
383
384
|
});
|
|
384
385
|
el.style.display = 'none';
|
|
385
386
|
|
|
386
387
|
// Label
|
|
387
388
|
this._label = createElement('span', { class: 'an-link-tooltip-url' });
|
|
388
|
-
this._label.textContent =
|
|
389
|
+
this._label.textContent = L.label;
|
|
389
390
|
el.appendChild(this._label);
|
|
390
391
|
|
|
391
392
|
el.appendChild(this._sep());
|
|
392
393
|
|
|
393
394
|
// Select-cells toggle
|
|
394
|
-
this._selectBtn = this._makeBtn(ICONS.selectCells,
|
|
395
|
+
this._selectBtn = this._makeBtn(ICONS.selectCells, L.selectCells, () => this._toggleSelectMode());
|
|
395
396
|
el.appendChild(this._selectBtn);
|
|
396
397
|
|
|
397
398
|
el.appendChild(this._sep());
|
|
398
399
|
|
|
399
400
|
// Row operations
|
|
400
|
-
el.appendChild(this._makeBtn(ICONS.rowAbove,
|
|
401
|
-
el.appendChild(this._makeBtn(ICONS.rowBelow,
|
|
402
|
-
el.appendChild(this._makeBtn(ICONS.deleteRow,
|
|
401
|
+
el.appendChild(this._makeBtn(ICONS.rowAbove, L.addRowAbove, () => this._addRow('above')));
|
|
402
|
+
el.appendChild(this._makeBtn(ICONS.rowBelow, L.addRowBelow, () => this._addRow('below')));
|
|
403
|
+
el.appendChild(this._makeBtn(ICONS.deleteRow, L.deleteRow, () => this._deleteRow()));
|
|
403
404
|
|
|
404
405
|
el.appendChild(this._sep());
|
|
405
406
|
|
|
406
407
|
// Column operations
|
|
407
|
-
el.appendChild(this._makeBtn(ICONS.colLeft,
|
|
408
|
-
el.appendChild(this._makeBtn(ICONS.colRight,
|
|
409
|
-
el.appendChild(this._makeBtn(ICONS.deleteCol,
|
|
408
|
+
el.appendChild(this._makeBtn(ICONS.colLeft, L.addColumnLeft, () => this._addColumn('left')));
|
|
409
|
+
el.appendChild(this._makeBtn(ICONS.colRight, L.addColumnRight, () => this._addColumn('right')));
|
|
410
|
+
el.appendChild(this._makeBtn(ICONS.deleteCol, L.deleteColumn, () => this._deleteColumn()));
|
|
410
411
|
|
|
411
412
|
el.appendChild(this._sep());
|
|
412
413
|
|
|
413
414
|
// Merge cells
|
|
414
|
-
el.appendChild(this._makeBtn(ICONS.mergeCells,
|
|
415
|
-
el.appendChild(this._makeBtn(ICONS.unmergeCells,
|
|
415
|
+
el.appendChild(this._makeBtn(ICONS.mergeCells, L.mergeCells, () => this._mergeCells()));
|
|
416
|
+
el.appendChild(this._makeBtn(ICONS.unmergeCells, L.unmergeCells, () => this._unmergeCells()));
|
|
416
417
|
|
|
417
418
|
el.appendChild(this._sep());
|
|
418
419
|
|
|
419
420
|
// Resize
|
|
420
|
-
el.appendChild(this._makeBtn(ICONS.colWidth,
|
|
421
|
-
el.appendChild(this._makeBtn(ICONS.rowHeight,
|
|
422
|
-
el.appendChild(this._makeBtn(ICONS.tableBorder,
|
|
421
|
+
el.appendChild(this._makeBtn(ICONS.colWidth, L.columnWidth, () => this._openSizePopover('col')));
|
|
422
|
+
el.appendChild(this._makeBtn(ICONS.rowHeight, L.rowHeight, () => this._openSizePopover('row')));
|
|
423
|
+
el.appendChild(this._makeBtn(ICONS.tableBorder,L.tableBorderWidth, () => this._openSizePopover('border')));
|
|
423
424
|
|
|
424
425
|
el.appendChild(this._sep());
|
|
425
426
|
|
|
426
427
|
// Delete table (danger)
|
|
427
|
-
el.appendChild(this._makeBtn(ICONS.deleteTable,
|
|
428
|
+
el.appendChild(this._makeBtn(ICONS.deleteTable, L.deleteTable, () => this._deleteTable(), true));
|
|
428
429
|
|
|
429
430
|
// Keep tooltip alive while hovering.
|
|
430
431
|
// Don't schedule hide on mouseleave when the size popover is open —
|
|
@@ -909,9 +910,9 @@ export class TableTooltip {
|
|
|
909
910
|
|
|
910
911
|
const actionsEl = createElement('div', { class: 'an-size-popover-actions' });
|
|
911
912
|
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
912
|
-
cancelBtn.textContent =
|
|
913
|
+
cancelBtn.textContent = this.context.locale.tooltips.table.cancelBtn;
|
|
913
914
|
const applyBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
914
|
-
applyBtn.textContent =
|
|
915
|
+
applyBtn.textContent = this.context.locale.tooltips.table.applyBtn;
|
|
915
916
|
actionsEl.appendChild(cancelBtn);
|
|
916
917
|
actionsEl.appendChild(applyBtn);
|
|
917
918
|
|
|
@@ -960,7 +961,7 @@ export class TableTooltip {
|
|
|
960
961
|
? (parseInt(firstCell.style.borderWidth, 10) ||
|
|
961
962
|
parseInt(window.getComputedStyle(firstCell).borderWidth, 10) || 1)
|
|
962
963
|
: 1;
|
|
963
|
-
this._sizeTitleEl.textContent =
|
|
964
|
+
this._sizeTitleEl.textContent = this.context.locale.tooltips.table.tableBorderWidthPx;
|
|
964
965
|
this._sizeInputEl.min = '0';
|
|
965
966
|
this._sizeInputEl.max = '10';
|
|
966
967
|
this._sizeInputEl.value = currentPx;
|
|
@@ -979,7 +980,9 @@ export class TableTooltip {
|
|
|
979
980
|
const t = c.closest('table');
|
|
980
981
|
return t && t === cell.closest('table');
|
|
981
982
|
});
|
|
982
|
-
this._sizeTitleEl.textContent = isCol
|
|
983
|
+
this._sizeTitleEl.textContent = isCol
|
|
984
|
+
? this.context.locale.tooltips.table.columnWidthPx
|
|
985
|
+
: this.context.locale.tooltips.table.rowHeightPx;
|
|
983
986
|
this._sizeInputEl.min = '1';
|
|
984
987
|
this._sizeInputEl.max = '2000';
|
|
985
988
|
this._sizeInputEl.value = isCol
|
package/src/js/module/Toolbar.js
CHANGED
|
@@ -182,9 +182,9 @@ export class Toolbar {
|
|
|
182
182
|
const btn = createElement('button', {
|
|
183
183
|
type: 'button',
|
|
184
184
|
class: baseClass,
|
|
185
|
-
title: def.tooltip || '',
|
|
185
|
+
title: this.context.locale.toolbar[def.name] || def.tooltip || '',
|
|
186
186
|
'data-btn': def.name,
|
|
187
|
-
'aria-label': def.tooltip || def.name,
|
|
187
|
+
'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
|
|
188
188
|
'aria-haspopup': 'true',
|
|
189
189
|
'aria-expanded': 'false',
|
|
190
190
|
});
|
|
@@ -206,7 +206,7 @@ export class Toolbar {
|
|
|
206
206
|
});
|
|
207
207
|
const grid = createElement('div', { class: 'an-table-grid' });
|
|
208
208
|
const label = createElement('div', { class: 'an-table-label' });
|
|
209
|
-
label.textContent = 'Insert Table';
|
|
209
|
+
label.textContent = this.context.locale.toolbar.insertTableLabel || 'Insert Table';
|
|
210
210
|
|
|
211
211
|
const cells = [];
|
|
212
212
|
for (let r = 1; r <= ROWS; r++) {
|
|
@@ -232,7 +232,7 @@ export class Toolbar {
|
|
|
232
232
|
const c = +cell.getAttribute('data-col');
|
|
233
233
|
cell.classList.toggle('active', r <= rows && c <= cols);
|
|
234
234
|
});
|
|
235
|
-
label.textContent = (rows && cols) ? `${rows} × ${cols}` : 'Insert Table';
|
|
235
|
+
label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
|
|
236
236
|
};
|
|
237
237
|
|
|
238
238
|
const openPopup = () => {
|
|
@@ -307,9 +307,9 @@ export class Toolbar {
|
|
|
307
307
|
const applyBtn = createElement('button', {
|
|
308
308
|
type: 'button',
|
|
309
309
|
class: `${baseClass} an-color-btn`,
|
|
310
|
-
title: def.tooltip || '',
|
|
310
|
+
title: this.context.locale.toolbar[def.name] || def.tooltip || '',
|
|
311
311
|
'data-btn': def.name,
|
|
312
|
-
'aria-label': def.tooltip || def.name,
|
|
312
|
+
'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
@@ -326,7 +326,9 @@ export class Toolbar {
|
|
|
326
326
|
const arrowBtn = createElement('button', {
|
|
327
327
|
type: 'button',
|
|
328
328
|
class: `${baseClass} an-color-arrow`,
|
|
329
|
-
title:
|
|
329
|
+
title: def.name === 'foreColor'
|
|
330
|
+
? (this.context.locale.toolbar.chooseTextColor || 'Choose text color')
|
|
331
|
+
: (this.context.locale.toolbar.chooseHighlightColor || 'Choose highlight color'),
|
|
330
332
|
'aria-haspopup': 'true',
|
|
331
333
|
'aria-expanded': 'false',
|
|
332
334
|
});
|
|
@@ -346,8 +348,8 @@ export class Toolbar {
|
|
|
346
348
|
});
|
|
347
349
|
|
|
348
350
|
const customRow = createElement('div', { class: 'an-color-custom' });
|
|
349
|
-
const colorInput = createElement('input', { type: 'color', value: currentColor, title: 'Custom color' });
|
|
350
|
-
const customLabel = createElement('span', {}, ['Custom color']);
|
|
351
|
+
const colorInput = createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' });
|
|
352
|
+
const customLabel = createElement('span', {}, [this.context.locale.toolbar.customColor || 'Custom color']);
|
|
351
353
|
customRow.appendChild(colorInput);
|
|
352
354
|
customRow.appendChild(customLabel);
|
|
353
355
|
|
|
@@ -491,19 +493,23 @@ export class Toolbar {
|
|
|
491
493
|
const cls = def.selectClass ? `an-select ${def.selectClass}` : 'an-select';
|
|
492
494
|
const select = createElement('select', {
|
|
493
495
|
class: cls,
|
|
494
|
-
title: def.tooltip || '',
|
|
496
|
+
title: this.context.locale.toolbar[def.name] || def.tooltip || '',
|
|
495
497
|
'data-btn': def.name,
|
|
496
|
-
'aria-label': def.tooltip || def.name,
|
|
498
|
+
'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
|
|
497
499
|
});
|
|
498
500
|
|
|
499
501
|
// Blank "placeholder" option (non-selectable header)
|
|
500
|
-
const placeholderText = def.placeholder || 'Font';
|
|
502
|
+
const placeholderText = this.context.locale.toolbar[def.name + 'Placeholder'] || def.placeholder || 'Font';
|
|
501
503
|
const placeholder = createElement('option', { value: '', disabled: '', hidden: '' }, [placeholderText]);
|
|
502
504
|
select.appendChild(placeholder);
|
|
503
505
|
|
|
504
506
|
items.forEach((item) => {
|
|
505
507
|
const value = (typeof item === 'object') ? item.value : item;
|
|
506
|
-
const label = (typeof item === 'object')
|
|
508
|
+
const label = (typeof item === 'object')
|
|
509
|
+
? (def.name === 'paragraphStyle'
|
|
510
|
+
? ((this.context.locale.toolbar.paragraphItems || {})[item.value] || item.label)
|
|
511
|
+
: item.label)
|
|
512
|
+
: item;
|
|
507
513
|
const isHeader = (typeof item === 'object') && !!item.disabled;
|
|
508
514
|
const attrs = { value };
|
|
509
515
|
if (isHeader) attrs.disabled = '';
|
|
@@ -558,9 +564,9 @@ export class Toolbar {
|
|
|
558
564
|
const btn = createElement('button', {
|
|
559
565
|
type: 'button',
|
|
560
566
|
class: classAttr,
|
|
561
|
-
title: btnDef.tooltip || '',
|
|
567
|
+
title: this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || '',
|
|
562
568
|
'data-btn': btnDef.name,
|
|
563
|
-
'aria-label': btnDef.tooltip || btnDef.name,
|
|
569
|
+
'aria-label': this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || btnDef.name,
|
|
564
570
|
});
|
|
565
571
|
|
|
566
572
|
// Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
|
|
@@ -59,24 +59,25 @@ export class VideoDialog {
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
61
|
_buildDialog() {
|
|
62
|
+
const L = this.context.locale.videoDialog;
|
|
62
63
|
const overlay = createElement('div', {
|
|
63
64
|
class: 'an-dialog-overlay',
|
|
64
65
|
role: 'dialog',
|
|
65
66
|
'aria-modal': 'true',
|
|
66
|
-
'aria-label':
|
|
67
|
+
'aria-label': L.ariaLabel,
|
|
67
68
|
});
|
|
68
69
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
69
70
|
|
|
70
71
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
71
|
-
title.textContent =
|
|
72
|
+
title.textContent = L.title;
|
|
72
73
|
|
|
73
74
|
// URL input
|
|
74
75
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
75
|
-
urlLabel.textContent =
|
|
76
|
+
urlLabel.textContent = L.videoUrl;
|
|
76
77
|
const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
77
78
|
type: 'url',
|
|
78
79
|
class: 'an-input',
|
|
79
|
-
placeholder:
|
|
80
|
+
placeholder: L.urlPlaceholder,
|
|
80
81
|
autocomplete: 'off',
|
|
81
82
|
}));
|
|
82
83
|
this._urlInput = urlInput;
|
|
@@ -87,7 +88,7 @@ export class VideoDialog {
|
|
|
87
88
|
|
|
88
89
|
// Width
|
|
89
90
|
const widthLabel = createElement('label', { class: 'an-label' });
|
|
90
|
-
widthLabel.textContent =
|
|
91
|
+
widthLabel.textContent = L.widthLabel;
|
|
91
92
|
const widthInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
92
93
|
type: 'number',
|
|
93
94
|
class: 'an-input',
|
|
@@ -101,9 +102,9 @@ export class VideoDialog {
|
|
|
101
102
|
// Buttons
|
|
102
103
|
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
103
104
|
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
104
|
-
insertBtn.textContent =
|
|
105
|
+
insertBtn.textContent = L.insertBtn;
|
|
105
106
|
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
106
|
-
cancelBtn.textContent =
|
|
107
|
+
cancelBtn.textContent = L.cancelBtn;
|
|
107
108
|
btnRow.appendChild(insertBtn);
|
|
108
109
|
btnRow.appendChild(cancelBtn);
|
|
109
110
|
|
|
@@ -113,7 +114,7 @@ export class VideoDialog {
|
|
|
113
114
|
// Live URL hint
|
|
114
115
|
const d0 = on(urlInput, 'input', () => {
|
|
115
116
|
const info = this._parseVideoUrl(urlInput.value.trim());
|
|
116
|
-
hintEl.textContent = info ?
|
|
117
|
+
hintEl.textContent = info ? this.context.locale.videoDialog.detected(info.type) : (urlInput.value ? this.context.locale.videoDialog.unknownFormat : '');
|
|
117
118
|
});
|
|
118
119
|
|
|
119
120
|
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
@@ -140,7 +141,7 @@ export class VideoDialog {
|
|
|
140
141
|
|
|
141
142
|
const html = this._buildEmbedHtml(rawUrl, width);
|
|
142
143
|
if (!html) {
|
|
143
|
-
this._hintEl.textContent =
|
|
144
|
+
this._hintEl.textContent = this.context.locale.videoDialog.invalidUrl;
|
|
144
145
|
this._urlInput.focus();
|
|
145
146
|
return;
|
|
146
147
|
}
|