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
@@ -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: `Choose ${def.name === 'foreColor' ? 'text' : 'highlight'} color`,
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') ? item.label : item;
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': 'Insert video',
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 = 'Insert Video';
72
+ title.textContent = L.title;
72
73
 
73
74
  // URL input
74
75
  const urlLabel = createElement('label', { class: 'an-label' });
75
- urlLabel.textContent = 'Video URL';
76
+ urlLabel.textContent = L.videoUrl;
76
77
  const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
77
78
  type: 'url',
78
79
  class: 'an-input',
79
- placeholder: 'YouTube, Vimeo, or direct .mp4 URL',
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 = 'Width (px)';
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 = 'Insert';
105
+ insertBtn.textContent = L.insertBtn;
105
106
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
106
- cancelBtn.textContent = 'Cancel';
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 ? `Detected: ${info.type}` : (urlInput.value ? 'Unknown format — will try direct video embed' : '');
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 = 'Invalid URL — please enter a valid video link.';
144
+ this._hintEl.textContent = this.context.locale.videoDialog.invalidUrl;
144
145
  this._urlInput.focus();
145
146
  return;
146
147
  }
@@ -78,24 +78,25 @@ export class VideoTooltip {
78
78
  // ---------------------------------------------------------------------------
79
79
 
80
80
  _buildTooltip() {
81
+ const L = this.context.locale.tooltips.video;
81
82
  const el = createElement('div', {
82
83
  class: 'an-link-tooltip an-video-tooltip',
83
84
  role: 'toolbar',
84
- 'aria-label': 'Video actions',
85
+ 'aria-label': L.ariaLabel,
85
86
  });
86
87
  el.style.display = 'none';
87
88
 
88
89
  // Label
89
90
  this._label = createElement('span', { class: 'an-link-tooltip-url' });
90
- this._label.textContent = 'Video';
91
+ this._label.textContent = L.label;
91
92
  el.appendChild(this._label);
92
93
 
93
94
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
94
95
 
95
- this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, 'Float Left', () => this._setFloat('left'));
96
- this._floatNoneBtn = this._makeBtn(ICONS.floatNone, 'No Float', () => this._setFloat(''));
97
- this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, 'Align Center', () => this._setCenter());
98
- this._floatRightBtn = this._makeBtn(ICONS.floatRight, 'Float Right', () => this._setFloat('right'));
96
+ this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, L.floatLeft, () => this._setFloat('left'));
97
+ this._floatNoneBtn = this._makeBtn(ICONS.floatNone, L.noFloat, () => this._setFloat(''));
98
+ this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, L.alignCenter, () => this._setCenter());
99
+ this._floatRightBtn = this._makeBtn(ICONS.floatRight, L.floatRight, () => this._setFloat('right'));
99
100
 
100
101
  el.appendChild(this._floatLeftBtn);
101
102
  el.appendChild(this._floatNoneBtn);
@@ -104,18 +105,18 @@ export class VideoTooltip {
104
105
 
105
106
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
106
107
 
107
- this._originalBtn = this._makeBtn(ICONS.originalSize, 'Original Size', () => this._resetSize());
108
+ this._originalBtn = this._makeBtn(ICONS.originalSize, L.originalSize, () => this._resetSize());
108
109
 
109
110
  el.appendChild(this._originalBtn);
110
111
 
111
112
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
112
113
 
113
- this._previewBtn = this._makeBtn(ICONS.preview, 'Preview Video', () => this._togglePreview());
114
+ this._previewBtn = this._makeBtn(ICONS.preview, L.previewVideo, () => this._togglePreview());
114
115
  el.appendChild(this._previewBtn);
115
116
 
116
117
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
117
118
 
118
- this._deleteBtn = this._makeBtn(ICONS.deleteVideo, 'Delete Video', () => this._delete(), true);
119
+ this._deleteBtn = this._makeBtn(ICONS.deleteVideo, L.deleteVideo, () => this._delete(), true);
119
120
 
120
121
  el.appendChild(this._deleteBtn);
121
122
 
@@ -292,7 +293,7 @@ export class VideoTooltip {
292
293
 
293
294
  // Visual feedback: button turns primary-coloured
294
295
  this._previewBtn.classList.add('an-link-tooltip-btn--copied');
295
- this._previewBtn.title = 'Exit Preview';
296
+ this._previewBtn.title = this.context.locale.tooltips.video.exitPreview;
296
297
 
297
298
  // Exit preview on mousedown outside the wrapper.
298
299
  // Using 'mousedown' (not 'click') so clicks inside an iframe
@@ -319,7 +320,7 @@ export class VideoTooltip {
319
320
 
320
321
  // Reset button appearance
321
322
  this._previewBtn.classList.remove('an-link-tooltip-btn--copied');
322
- this._previewBtn.title = 'Preview Video';
323
+ this._previewBtn.title = this.context.locale.tooltips.video.previewVideo;
323
324
 
324
325
  if (this._previewClickOff) {
325
326
  document.removeEventListener('mousedown', this._previewClickOff, true);
@@ -135,4 +135,8 @@ export const defaultOptions = {
135
135
  // Custom focus ring colour — overrides the default blue when set.
136
136
  // Accepts any valid CSS colour string, e.g. '#f97316', 'hsl(25,90%,55%)'.
137
137
  focusColor: null,
138
+ // Display language for the editor UI.
139
+ // Built-in values: 'en' (default), 'vi', 'ja', 'zh', 'fr'.
140
+ // Pass a partial or full locale object to override individual strings.
141
+ lang: 'en',
138
142
  };
@@ -48,20 +48,34 @@
48
48
  .an-editable {
49
49
  cursor: text;
50
50
  user-select: text;
51
- // Subtle visual cue that the area is read-only.
52
- background: #fafafa;
51
+ // No background change keep the same appearance as editable mode
52
+ // so read-only content looks identical to the live editor.
53
53
 
54
54
  // Block checklist checkbox interaction in read-only mode.
55
+ // Keep opacity:1 so disabled checkboxes are still clearly visible.
55
56
  ul.an-checklist input[type='checkbox'] {
56
57
  pointer-events: none;
57
58
  cursor: default;
59
+ opacity: 1;
58
60
  }
59
61
 
60
- // Prevent images and videos from being dragged out.
61
- img,
62
+ // Prevent images from being dragged out.
63
+ img {
64
+ -webkit-user-drag: none;
65
+ user-drag: none;
66
+ }
67
+
68
+ // Prevent video wrapper itself from being dragged, but keep pointer-events
69
+ // active so the video shield works and the iframe is visible/interactive.
62
70
  .an-video-wrapper {
63
71
  -webkit-user-drag: none;
64
72
  user-drag: none;
73
+ }
74
+
75
+ // D-3: In read-only mode, disable the video shield so clicks reach the
76
+ // underlying <iframe> / <video> and users can play videos normally.
77
+ // The shield is only needed in edit mode to prevent accidental interaction.
78
+ .an-video-wrapper .an-video-shield {
65
79
  pointer-events: none;
66
80
  }
67
81
  }
@@ -435,7 +449,7 @@
435
449
  // Content styles
436
450
  p { margin: 0 0 0.75em; }
437
451
  h1, h2, h3, h4, h5, h6 { margin: 0.5em 0; line-height: 1.3; }
438
- ul, ol { margin: 0 0 0.75em 1.5em; padding: 0; }
452
+ ul, ol { margin: 0 0 0.75em 0; padding-left: 1.5em; list-style-position: inside; }
439
453
  li { margin-bottom: 0.25em; }
440
454
  blockquote {
441
455
  margin: 0.5em 0 0.5em 1em;
@@ -638,6 +652,26 @@
638
652
  letter-spacing: 0.02em;
639
653
  }
640
654
 
655
+ // Active state for toggle buttons in tooltips (e.g. Select Cells)
656
+ .an-link-tooltip-btn--active {
657
+ background: rgba(#3b82f6, 0.12) !important;
658
+ color: #3b82f6 !important;
659
+ svg { stroke: #3b82f6; }
660
+ }
661
+
662
+ // Cells highlighted when select mode is active
663
+ .an-cell-selected {
664
+ background-color: rgba(#3b82f6, 0.18) !important;
665
+ outline: 2px solid #3b82f6;
666
+ outline-offset: -2px;
667
+ }
668
+
669
+ // Cursor hint for all cells when select mode is on
670
+ .an-table-select-mode td,
671
+ .an-table-select-mode th {
672
+ cursor: cell;
673
+ }
674
+
641
675
  // Size popover (column width / row height)
642
676
  .an-size-popover {
643
677
  position: fixed;
@@ -1759,6 +1793,17 @@ $an-video-accent: #8b5cf6; // violet-500
1759
1793
  color: #cdd6f4;
1760
1794
  }
1761
1795
 
1796
+ .an-link-tooltip-btn--active {
1797
+ background: rgba(#6366f1, 0.2) !important;
1798
+ color: #818cf8 !important;
1799
+ svg { stroke: #818cf8; }
1800
+ }
1801
+
1802
+ .an-cell-selected {
1803
+ background-color: rgba(#6366f1, 0.25) !important;
1804
+ outline-color: #6366f1;
1805
+ }
1806
+
1762
1807
  .an-table-cell {
1763
1808
  background: #24273a;
1764
1809
  border-color: #3f3f5f;
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * AutumnNote – TypeScript declarations
3
- * @version 1.0.4
3
+ * @version 1.0.9
4
4
  */
5
5
 
6
6
  // ---------------------------------------------------------------------------
@@ -104,6 +104,12 @@ export interface AsnOptions {
104
104
  onPaste?: (data: { text: string; html: string | null }) => void;
105
105
  /** Additional color swatches shown at the top of the color picker. */
106
106
  colorSwatches?: string[];
107
+ /**
108
+ * Display language for the editor UI.
109
+ * Built-in: 'en' (default) | 'vi' | 'ja' | 'zh' | 'fr'.
110
+ * Pass a partial locale object to override individual strings.
111
+ */
112
+ lang?: string | Partial<AsnLocale>;
107
113
  }
108
114
 
109
115
  // ---------------------------------------------------------------------------
@@ -179,6 +185,57 @@ export type ToolbarItemDef = ButtonDef | DropdownDef | GridButtonDef | ColorPick
179
185
  /** Runtime default options object (same export as `src/js/index.js`). */
180
186
  export declare const defaultOptions: Required<AsnOptions>;
181
187
 
188
+ // ---------------------------------------------------------------------------
189
+ // Locale / i18n
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /** Full locale object shape used by the editor. All leaf values are strings or template functions. */
193
+ export interface AsnLocale {
194
+ toolbar: Record<string, string | ((n: string | number) => string) | Record<string, string>>;
195
+ linkDialog: Record<string, string>;
196
+ imageDialog: Record<string, string>;
197
+ videoDialog: Record<string, string | ((type: string) => string)>;
198
+ emojiDialog: Record<string, string | Record<string, string>>;
199
+ iconDialog: Record<string, string | Record<string, string>>;
200
+ findReplace: Record<string, string>;
201
+ shortcutsDialog: {
202
+ title: string;
203
+ ariaLabel: string;
204
+ close: string;
205
+ shortcuts: Array<{ category: string; items: Array<{ keys: string; action: string }> }>;
206
+ };
207
+ contextMenu: Record<string, string>;
208
+ statusbar: {
209
+ resizeHandle: string;
210
+ words: (n: number) => string;
211
+ wordsLimit: (n: number, max: number) => string;
212
+ chars: (n: number) => string;
213
+ charsLimit: (n: number, max: number) => string;
214
+ };
215
+ tooltips: {
216
+ link: Record<string, string>;
217
+ image: Record<string, string>;
218
+ code: Record<string, string>;
219
+ table: Record<string, string>;
220
+ video: Record<string, string>;
221
+ };
222
+ errors: {
223
+ imageFormat: (type: string) => string;
224
+ imageSize: (maxMb: number) => string;
225
+ };
226
+ }
227
+
228
+ /** Registry of all built-in locales (keys: 'en', 'vi', 'ja', 'zh', 'fr'). */
229
+ export declare const locales: Record<string, AsnLocale>;
230
+
231
+ /**
232
+ * Resolves a lang value to a full AsnLocale.
233
+ * - string key → looks up locales registry, deep-merges with 'en' fallback.
234
+ * - object → deep-merges with 'en' as base.
235
+ * - falsy / 'en' → returns the canonical English locale.
236
+ */
237
+ export declare function resolveLocale(lang: string | Partial<AsnLocale> | null | undefined): AsnLocale;
238
+
182
239
  // Re-export core utility modules exposed by the package entry-point.
183
240
  export * from '../src/js/core/dom.js';
184
241
  export * from '../src/js/core/range.js';