autumnnote 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +332 -638
  2. package/dist/autumnnote.css +40 -4
  3. package/dist/autumnnote.es.js +3375 -262
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +3374 -261
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/js/Context.js +4 -0
  9. package/src/js/editing/Style.js +195 -44
  10. package/src/js/editing/Typing.js +2 -2
  11. package/src/js/i18n/de.js +320 -0
  12. package/src/js/i18n/en.js +328 -0
  13. package/src/js/i18n/es.js +320 -0
  14. package/src/js/i18n/fr.js +321 -0
  15. package/src/js/i18n/index.js +59 -0
  16. package/src/js/i18n/ja.js +321 -0
  17. package/src/js/i18n/ko.js +320 -0
  18. package/src/js/i18n/vi.js +321 -0
  19. package/src/js/i18n/zh.js +321 -0
  20. package/src/js/index.js +2 -1
  21. package/src/js/module/CodeTooltip.js +12 -9
  22. package/src/js/module/ContextMenu.js +13 -11
  23. package/src/js/module/Editor.js +14 -3
  24. package/src/js/module/EmojiDialog.js +19 -7
  25. package/src/js/module/FindReplace.js +14 -13
  26. package/src/js/module/IconDialog.js +25 -13
  27. package/src/js/module/ImageDialog.js +25 -20
  28. package/src/js/module/ImageTooltip.js +13 -12
  29. package/src/js/module/LinkDialog.js +10 -9
  30. package/src/js/module/LinkTooltip.js +6 -5
  31. package/src/js/module/Placeholder.js +6 -1
  32. package/src/js/module/ShortcutsDialog.js +5 -4
  33. package/src/js/module/Statusbar.js +6 -5
  34. package/src/js/module/TableTooltip.js +412 -102
  35. package/src/js/module/Toolbar.js +21 -15
  36. package/src/js/module/VideoDialog.js +10 -9
  37. package/src/js/module/VideoTooltip.js +12 -11
  38. package/src/js/settings.js +4 -0
  39. package/src/styles/autumnnote.scss +50 -5
  40. package/types/index.d.ts +58 -1
@@ -0,0 +1,321 @@
1
+ /**
2
+ * zh.js - 简体中文 (Simplified Chinese) locale
3
+ * 仅包含与 en.js 不同的键值。
4
+ * resolveLocale 会将本文件与 en.js 进行深度合并。
5
+ */
6
+
7
+ /** @type {Partial<import('../../../types/index.js').AsnLocale>} */
8
+ export const zh = {
9
+ toolbar: {
10
+ bold: '粗体 (Ctrl+B)',
11
+ italic: '斜体 (Ctrl+I)',
12
+ underline: '下划线 (Ctrl+U)',
13
+ strikethrough: '删除线',
14
+ superscript: '上标',
15
+ subscript: '下标',
16
+ alignLeft: '左对齐',
17
+ alignCenter: '居中',
18
+ alignRight: '右对齐',
19
+ alignJustify: '两端对齐',
20
+ ul: '无序列表',
21
+ ol: '有序列表',
22
+ checklist: '待办列表',
23
+ indent: '增加缩进',
24
+ outdent: '减少缩进',
25
+ undo: '撤销 (Ctrl+Z)',
26
+ redo: '重做 (Ctrl+Y)',
27
+ hr: '水平分割线',
28
+ link: '插入链接',
29
+ image: '插入图片',
30
+ video: '插入视频',
31
+ emoji: '插入表情',
32
+ icon: '插入 FA 图标',
33
+ table: '插入表格',
34
+ fontSize: '字号',
35
+ fontSizePlaceholder: '大小',
36
+ removeFormat: '清除格式',
37
+ direction: '切换文字方向 (LTR / RTL)',
38
+ fontFamily: '字体',
39
+ paragraphStyle: '段落样式',
40
+ paragraphStylePlaceholder: '样式',
41
+ lineHeight: '行高',
42
+ lineHeightPlaceholder: '\u2195 行距',
43
+ codeview: '查看 HTML 源码',
44
+ fullscreen: '全屏',
45
+ shortcuts: '键盘快捷键 (Ctrl+Shift+/)',
46
+ find: '搜索 (Ctrl+F)',
47
+ findReplace: '查找和替换 (Ctrl+H)',
48
+ inlineCode: '行内代码 (Ctrl+`)',
49
+ print: '打印',
50
+ foreColor: '文字颜色',
51
+ backColor: '高亮颜色',
52
+ chooseTextColor: '选择文字颜色',
53
+ chooseHighlightColor: '选择高亮颜色',
54
+ customColor: '自定义颜色',
55
+ insertTableLabel: '插入表格',
56
+ paragraphItems: {
57
+ p: '正文',
58
+ blockquote: '引用',
59
+ pre: '代码',
60
+ },
61
+ },
62
+
63
+ linkDialog: {
64
+ ariaLabel: '插入链接',
65
+ title: '插入链接',
66
+ url: '链接地址',
67
+ urlPlaceholder: 'https://',
68
+ displayText: '显示文字',
69
+ textPlaceholder: '链接文字',
70
+ openInNewTab: '在新标签页中打开',
71
+ insertBtn: '插入',
72
+ cancelBtn: '取消',
73
+ },
74
+
75
+ imageDialog: {
76
+ ariaLabel: '插入图片',
77
+ title: '插入图片',
78
+ imageUrl: '图片地址',
79
+ urlPlaceholder: 'https://example.com/image.png',
80
+ altText: '替代文字',
81
+ altPlaceholder: '图片描述',
82
+ alignment: '对齐方式',
83
+ alignNone: '无',
84
+ alignLeft: '左',
85
+ alignCenter: '居中',
86
+ alignRight: '右',
87
+ uploadLabel: '或上传文件',
88
+ insertBtn: '插入',
89
+ cancelBtn: '取消',
90
+ },
91
+
92
+ videoDialog: {
93
+ ariaLabel: '插入视频',
94
+ title: '插入视频',
95
+ videoUrl: '视频地址',
96
+ urlPlaceholder: 'YouTube、Vimeo 或直接 .mp4 链接',
97
+ widthLabel: '宽度 (px)',
98
+ widthPlaceholder: '560',
99
+ insertBtn: '插入',
100
+ cancelBtn: '取消',
101
+ detected: (type) => `已识别: ${type}`,
102
+ unknownFormat: '未知格式 \u2014 将尝试直接嵌入视频',
103
+ invalidUrl: 'URL 无效 \u2014 请输入有效的视频链接。',
104
+ },
105
+
106
+ emojiDialog: {
107
+ ariaLabel: '插入表情',
108
+ title: '插入表情',
109
+ searchPlaceholder: '搜索表情\u2026',
110
+ all: '全部',
111
+ cancelBtn: '取消',
112
+ close: '关闭',
113
+ categories: {
114
+ smileys: '笑脸',
115
+ people: '人物',
116
+ animals: '动物',
117
+ food: '食物',
118
+ travel: '旅行',
119
+ objects: '物品',
120
+ symbols: '符号',
121
+ },
122
+ },
123
+
124
+ iconDialog: {
125
+ ariaLabel: '插入 FA 图标',
126
+ title: '插入 FA 图标',
127
+ searchPlaceholder: '搜索图标\u2026',
128
+ all: '全部',
129
+ style: '样式',
130
+ size: '大小',
131
+ color: '颜色',
132
+ useColor: ' 使用颜色',
133
+ selectHint: '请选择一个图标',
134
+ insertBtn: '插入 FA 图标',
135
+ cancelBtn: '取消',
136
+ close: '关闭',
137
+ categories: {
138
+ popular: '热门',
139
+ interface: '界面',
140
+ navigation: '导航',
141
+ media: '媒体',
142
+ communication: '通讯',
143
+ files: '文件',
144
+ people: '人物',
145
+ objects: '物品',
146
+ },
147
+ },
148
+
149
+ findReplace: {
150
+ findTitle: '搜索',
151
+ findReplaceTitle: '查找和替换',
152
+ findPlaceholder: '查找\u2026',
153
+ searchAriaLabel: '搜索文字',
154
+ caseSensitive: '\u00a0区分大小写',
155
+ prevBtn: '\u2190 上一个',
156
+ nextBtn: '下一个 \u2192',
157
+ replacePlaceholder: '替换为\u2026',
158
+ replaceAriaLabel: '替换为',
159
+ replaceBtn: '替换',
160
+ replaceAllBtn: '全部替换',
161
+ close: '\u00d7',
162
+ },
163
+
164
+ shortcutsDialog: {
165
+ title: '键盘快捷键',
166
+ ariaLabel: '键盘快捷键',
167
+ close: '关闭',
168
+ shortcuts: [
169
+ {
170
+ category: '文字格式',
171
+ items: [
172
+ { keys: 'Ctrl + B', action: '粗体' },
173
+ { keys: 'Ctrl + I', action: '斜体' },
174
+ { keys: 'Ctrl + U', action: '下划线' },
175
+ { keys: 'Ctrl + K', action: '插入 / 编辑链接' },
176
+ ],
177
+ },
178
+ {
179
+ category: '历史记录',
180
+ items: [
181
+ { keys: 'Ctrl + Z', action: '撤销' },
182
+ { keys: 'Ctrl + Y / Ctrl + Shift + Z', action: '重做' },
183
+ ],
184
+ },
185
+ {
186
+ category: '选择与导航',
187
+ items: [
188
+ { keys: 'Ctrl + A', action: '全选' },
189
+ { keys: 'Tab', action: '增加缩进 / 插入空格' },
190
+ { keys: 'Shift + Tab', action: '减少缩进' },
191
+ ],
192
+ },
193
+ {
194
+ category: '剪贴板',
195
+ items: [
196
+ { keys: 'Ctrl + Shift + V', action: '粘贴为纯文本' },
197
+ ],
198
+ },
199
+ {
200
+ category: '查找和替换',
201
+ items: [
202
+ { keys: 'Ctrl + F', action: '在文档中搜索' },
203
+ { keys: 'Ctrl + H', action: '查找和替换' },
204
+ ],
205
+ },
206
+ {
207
+ category: '编辑器',
208
+ items: [
209
+ { keys: 'Ctrl + Shift + /', action: '显示快捷键对话框' },
210
+ ],
211
+ },
212
+ ],
213
+ },
214
+
215
+ contextMenu: {
216
+ cut: '剪切',
217
+ copy: '复制',
218
+ paste: '粘贴',
219
+ bold: '粗体',
220
+ italic: '斜体',
221
+ underline: '下划线',
222
+ textColor: '文字颜色',
223
+ highlightColor: '高亮颜色',
224
+ copyFormat: '复制格式',
225
+ pasteFormat: '粘贴格式',
226
+ removeFormat: '清除格式',
227
+ link: '插入链接',
228
+ image: '插入图片',
229
+ video: '插入视频',
230
+ table: '插入表格',
231
+ back: '返回',
232
+ noHighlight: '无高亮',
233
+ customColor: '自定义颜色',
234
+ customColorLabel: '自定义\u2026',
235
+ },
236
+
237
+ statusbar: {
238
+ resizeHandle: '拖动以调整大小',
239
+ words: (n) => `字数: ${n}`,
240
+ wordsLimit: (n, max) => `字数: ${n}/${max}`,
241
+ chars: (n) => `字符数: ${n}`,
242
+ charsLimit: (n, max) => `字符数: ${n}/${max}`,
243
+ },
244
+
245
+ tooltips: {
246
+ link: {
247
+ ariaLabel: '链接操作',
248
+ openLink: '打开链接',
249
+ copyUrl: '复制 URL',
250
+ editLink: '编辑链接',
251
+ removeLink: '删除链接',
252
+ },
253
+ image: {
254
+ ariaLabel: '图片操作',
255
+ label: '图片',
256
+ floatLeft: '左浮动',
257
+ noFloat: '不浮动',
258
+ alignCenter: '居中对齐',
259
+ floatRight: '右浮动',
260
+ originalSize: '原始尺寸',
261
+ rotateLeft: '向左旋转',
262
+ rotateRight: '向右旋转',
263
+ cropImage: '裁剪图片',
264
+ addCaption: '添加 / 编辑说明',
265
+ deleteImage: '删除图片',
266
+ },
267
+ code: {
268
+ ariaLabel: '代码块操作',
269
+ label: '代码',
270
+ syntaxLanguage: '语法语言',
271
+ syntaxAriaLabel: '语法语言',
272
+ copyCode: '复制代码',
273
+ toggleWordWrap: '切换自动换行',
274
+ enableWordWrap: '启用自动换行',
275
+ disableWordWrap: '禁用自动换行',
276
+ convertToParagraph: '转换为段落',
277
+ deleteCodeBlock: '删除代码块',
278
+ },
279
+ table: {
280
+ ariaLabel: '表格操作',
281
+ label: '表格',
282
+ selectCells: '选择单元格',
283
+ addRowAbove: '在上方插入行',
284
+ addRowBelow: '在下方插入行',
285
+ deleteRow: '删除行',
286
+ addColumnLeft: '在左侧插入列',
287
+ addColumnRight: '在右侧插入列',
288
+ deleteColumn: '删除列',
289
+ mergeCells: '合并单元格',
290
+ unmergeCells: '拆分单元格',
291
+ columnWidth: '列宽',
292
+ rowHeight: '行高',
293
+ tableBorderWidth: '表格边框宽度',
294
+ deleteTable: '删除表格',
295
+ columnWidthPx: '列宽 (px)',
296
+ rowHeightPx: '行高 (px)',
297
+ tableBorderWidthPx: '表格边框宽度 (px)',
298
+ cancelBtn: '取消',
299
+ applyBtn: '应用',
300
+ },
301
+ video: {
302
+ ariaLabel: '视频操作',
303
+ label: '视频',
304
+ floatLeft: '左浮动',
305
+ noFloat: '不浮动',
306
+ alignCenter: '居中对齐',
307
+ floatRight: '右浮动',
308
+ originalSize: '原始尺寸',
309
+ previewVideo: '预览视频',
310
+ exitPreview: '退出预览',
311
+ deleteVideo: '删除视频',
312
+ },
313
+ },
314
+
315
+ errors: {
316
+ imageFormat: (type) =>
317
+ `格式 "${type}" 不支持在浏览器中显示。请转换为 JPEG、PNG 或 WebP。`,
318
+ imageSize: (maxSize) =>
319
+ `图片文件过大。最大允许大小为 ${maxSize} MB。`,
320
+ },
321
+ };
package/src/js/index.js CHANGED
@@ -27,6 +27,7 @@ export * from './core/lists.js';
27
27
  export * from './core/env.js';
28
28
  export * from './core/sanitise.js';
29
29
  export * from './module/Buttons.js';
30
+ export { locales, resolveLocale } from './i18n/index.js';
30
31
 
31
32
  // ---------------------------------------------------------------------------
32
33
  // Main factory
@@ -99,7 +100,7 @@ const AutumnNote = {
99
100
  registerModule(name, ModuleClass) { _customModules.set(name, ModuleClass); },
100
101
 
101
102
  /** Library version */
102
- version: '1.0.4',
103
+ version: '1.0.9',
103
104
  };
104
105
 
105
106
  // ---------------------------------------------------------------------------
@@ -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': 'Code block actions',
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 = 'Code';
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: 'Syntax Language',
97
- 'aria-label': 'Syntax language',
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, 'Copy Code', () => this._copyCode());
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, 'Toggle Word Wrap', () => this._toggleWrap());
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, 'Convert to Paragraph', () => this._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, 'Delete Code Block', () => this._delete(), true));
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 ? 'Disable Word Wrap' : 'Enable Word Wrap';
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
- backBtn.appendChild(createElement('span', { class: 'an-context-label' }, [it.label || 'Back']));
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();
@@ -153,8 +153,19 @@ export class Editor {
153
153
  on(editable, 'click', onCheckboxClick),
154
154
  on(editable, 'mouseup', fixChecklistCursor),
155
155
  on(editable, 'keyup', fixChecklistCursor),
156
- // Block drag-out and external drops in read-only mode
157
- on(editable, 'dragstart', (e) => { if (isReadOnly()) e.preventDefault(); }),
156
+ // Block drag-out and external drops in read-only mode.
157
+ // D-1: Also block dragging of iframes and .an-video-wrapper elements in
158
+ // edit mode — a user can inadvertently drag the iframe out of its wrapper
159
+ // (making it playable/removable from contenteditable protection) by holding
160
+ // the mouse and moving outside the wrapper before releasing.
161
+ on(editable, 'dragstart', (e) => {
162
+ if (isReadOnly()) { e.preventDefault(); return; }
163
+ const target = e.target;
164
+ if (target && (target.nodeName === 'IFRAME' ||
165
+ (target.closest && target.closest('.an-video-wrapper')))) {
166
+ e.preventDefault();
167
+ }
168
+ }),
158
169
  on(editable, 'drop', (e) => { if (isReadOnly()) e.preventDefault(); }),
159
170
  );
160
171
 
@@ -370,7 +381,7 @@ export class Editor {
370
381
  * @param {string} html - HTML string (will be sanitised)
371
382
  */
372
383
  setHTML(html) {
373
- this.context.layoutInfo.editable.innerHTML = sanitiseHTML(html);
384
+ this.context.layoutInfo.editable.innerHTML = sanitiseHTML(html, { allowIframes: true });
374
385
  if (this._history) this._history.reset();
375
386
  this.afterCommand();
376
387
  }
@@ -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': 'Insert emoji',
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 = 'Insert Emoji';
570
- const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': 'Close' });
570
+ title.textContent = L.title;
571
+ const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
571
572
  closeBtn.innerHTML = '&times;';
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: 'Search emojis…',
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 = 'All';
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 = 'Cancel';
615
+ cancelBtn.textContent = L.cancelBtn;
615
616
  btnRow.appendChild(cancelBtn);
616
617
 
617
618
  box.append(titleRow, searchInput, catBar, grid, btnRow);
@@ -688,7 +689,18 @@ export class EmojiDialog {
688
689
 
689
690
  // 2. Insert emoji as a plain text node — no ZWS or execCommand needed.
690
691
  // Text nodes are natively navigable so the caret lands cleanly after.
692
+ //
693
+ // E-1: When the range covers the entire content of a <td>/<th>,
694
+ // deleteContents() can drift the range endpoint outside the cell in some
695
+ // browsers. Save the cell reference first and re-anchor after deletion.
696
+ const _sc = range.startContainer;
697
+ const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
698
+ ?.closest?.('td, th');
691
699
  range.deleteContents();
700
+ if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
701
+ range.setStart(_tdAnchor, 0);
702
+ range.collapse(true);
703
+ }
692
704
  const textNode = document.createTextNode(char);
693
705
  range.insertNode(textNode);
694
706