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
|
@@ -332,11 +332,12 @@ export class IconDialog {
|
|
|
332
332
|
// ---------------------------------------------------------------------------
|
|
333
333
|
|
|
334
334
|
_buildDialog() {
|
|
335
|
+
const L = this.context.locale.iconDialog;
|
|
335
336
|
const overlay = createElement('div', {
|
|
336
337
|
class: 'an-dialog-overlay',
|
|
337
338
|
role: 'dialog',
|
|
338
339
|
'aria-modal': 'true',
|
|
339
|
-
'aria-label':
|
|
340
|
+
'aria-label': L.ariaLabel,
|
|
340
341
|
});
|
|
341
342
|
|
|
342
343
|
const box = createElement('div', { class: 'an-dialog-box an-icon-box' });
|
|
@@ -344,8 +345,8 @@ export class IconDialog {
|
|
|
344
345
|
// Title row
|
|
345
346
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
346
347
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
347
|
-
title.textContent =
|
|
348
|
-
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label':
|
|
348
|
+
title.textContent = L.title;
|
|
349
|
+
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
|
|
349
350
|
closeBtn.innerHTML = '×';
|
|
350
351
|
titleRow.append(title, closeBtn);
|
|
351
352
|
|
|
@@ -353,7 +354,7 @@ export class IconDialog {
|
|
|
353
354
|
const searchInput = createElement('input', {
|
|
354
355
|
type: 'search',
|
|
355
356
|
class: 'an-input an-icon-search',
|
|
356
|
-
placeholder:
|
|
357
|
+
placeholder: L.searchPlaceholder,
|
|
357
358
|
autocomplete: 'off',
|
|
358
359
|
});
|
|
359
360
|
this._searchInput = searchInput;
|
|
@@ -361,11 +362,11 @@ export class IconDialog {
|
|
|
361
362
|
// Category tabs
|
|
362
363
|
const catBar = createElement('div', { class: 'an-icon-cats' });
|
|
363
364
|
const allTab = createElement('button', { type: 'button', class: 'an-icon-cat active', 'data-cat': 'all' });
|
|
364
|
-
allTab.textContent =
|
|
365
|
+
allTab.textContent = L.all;
|
|
365
366
|
catBar.appendChild(allTab);
|
|
366
367
|
ICON_CATEGORIES.forEach(({ id, label }) => {
|
|
367
368
|
const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
|
|
368
|
-
tab.textContent = label;
|
|
369
|
+
tab.textContent = (L.categories && L.categories[id]) || label;
|
|
369
370
|
catBar.appendChild(tab);
|
|
370
371
|
});
|
|
371
372
|
this._catBar = catBar;
|
|
@@ -386,7 +387,7 @@ export class IconDialog {
|
|
|
386
387
|
const optRow = createElement('div', { class: 'an-icon-options' });
|
|
387
388
|
|
|
388
389
|
const styleLabel = createElement('label', { class: 'an-label' });
|
|
389
|
-
styleLabel.textContent =
|
|
390
|
+
styleLabel.textContent = L.style;
|
|
390
391
|
const styleSelect = createElement('select', { class: 'an-input an-icon-option-select' });
|
|
391
392
|
[['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
|
|
392
393
|
const opt = createElement('option', { value: v });
|
|
@@ -397,7 +398,7 @@ export class IconDialog {
|
|
|
397
398
|
this._styleSelect = styleSelect;
|
|
398
399
|
|
|
399
400
|
const sizeLabel = createElement('label', { class: 'an-label' });
|
|
400
|
-
sizeLabel.textContent =
|
|
401
|
+
sizeLabel.textContent = L.size;
|
|
401
402
|
const sizeSelect = createElement('select', { class: 'an-input an-icon-option-select' });
|
|
402
403
|
[['', 'Inherit'], ['0.75em', '0.75em'], ['1em', '1em'], ['1.25em', '1.25em'], ['1.5em', '1.5em'], ['2em', '2em'], ['3em', '3em']].forEach(([v, t]) => {
|
|
403
404
|
const opt = createElement('option', { value: v });
|
|
@@ -408,30 +409,30 @@ export class IconDialog {
|
|
|
408
409
|
this._sizeSelect = sizeSelect;
|
|
409
410
|
|
|
410
411
|
const colorLabel = createElement('label', { class: 'an-label' });
|
|
411
|
-
colorLabel.textContent =
|
|
412
|
+
colorLabel.textContent = L.color;
|
|
412
413
|
const colorInput = createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' });
|
|
413
414
|
this._colorInput = colorInput;
|
|
414
415
|
|
|
415
416
|
const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
|
|
416
417
|
const useColorCb = createElement('input', { type: 'checkbox', checked: '' });
|
|
417
418
|
this._useColorCb = useColorCb;
|
|
418
|
-
useColorLabel.append(useColorCb, document.createTextNode(
|
|
419
|
+
useColorLabel.append(useColorCb, document.createTextNode(L.useColor));
|
|
419
420
|
|
|
420
421
|
optRow.append(styleLabel, styleSelect, sizeLabel, sizeSelect, colorLabel, colorInput, useColorLabel);
|
|
421
422
|
|
|
422
423
|
// Preview
|
|
423
424
|
const preview = createElement('div', { class: 'an-icon-preview' });
|
|
424
425
|
const previewHint = createElement('span', { class: 'an-icon-preview-hint' });
|
|
425
|
-
previewHint.textContent =
|
|
426
|
+
previewHint.textContent = L.selectHint;
|
|
426
427
|
preview.appendChild(previewHint);
|
|
427
428
|
this._preview = preview;
|
|
428
429
|
|
|
429
430
|
// Actions
|
|
430
431
|
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
431
432
|
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary', disabled: '' });
|
|
432
|
-
insertBtn.textContent =
|
|
433
|
+
insertBtn.textContent = L.insertBtn;
|
|
433
434
|
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
434
|
-
cancelBtn.textContent =
|
|
435
|
+
cancelBtn.textContent = L.cancelBtn;
|
|
435
436
|
btnRow.append(insertBtn, cancelBtn);
|
|
436
437
|
this._insertBtn = insertBtn;
|
|
437
438
|
|
|
@@ -57,35 +57,36 @@ export class ImageDialog {
|
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
|
|
59
59
|
_buildDialog() {
|
|
60
|
+
const L = this.context.locale.imageDialog;
|
|
60
61
|
const overlay = createElement('div', {
|
|
61
62
|
class: 'an-dialog-overlay',
|
|
62
63
|
role: 'dialog',
|
|
63
64
|
'aria-modal': 'true',
|
|
64
|
-
'aria-label':
|
|
65
|
+
'aria-label': L.ariaLabel,
|
|
65
66
|
});
|
|
66
67
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
67
68
|
|
|
68
69
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
69
|
-
title.textContent =
|
|
70
|
+
title.textContent = L.title;
|
|
70
71
|
|
|
71
72
|
// URL tab
|
|
72
73
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
73
|
-
urlLabel.textContent =
|
|
74
|
+
urlLabel.textContent = L.imageUrl;
|
|
74
75
|
const urlInput = createElement('input', {
|
|
75
76
|
type: 'url',
|
|
76
77
|
class: 'an-input',
|
|
77
|
-
placeholder:
|
|
78
|
+
placeholder: L.urlPlaceholder,
|
|
78
79
|
autocomplete: 'off',
|
|
79
80
|
});
|
|
80
81
|
this._urlInput = urlInput;
|
|
81
82
|
|
|
82
83
|
// Alt text
|
|
83
84
|
const altLabel = createElement('label', { class: 'an-label' });
|
|
84
|
-
altLabel.textContent =
|
|
85
|
+
altLabel.textContent = L.altText;
|
|
85
86
|
const altInput = createElement('input', {
|
|
86
87
|
type: 'text',
|
|
87
88
|
class: 'an-input',
|
|
88
|
-
placeholder:
|
|
89
|
+
placeholder: L.altPlaceholder,
|
|
89
90
|
autocomplete: 'off',
|
|
90
91
|
});
|
|
91
92
|
this._altInput = altInput;
|
|
@@ -94,13 +95,13 @@ export class ImageDialog {
|
|
|
94
95
|
|
|
95
96
|
// Alignment
|
|
96
97
|
const alignLabel = createElement('label', { class: 'an-label' });
|
|
97
|
-
alignLabel.textContent =
|
|
98
|
+
alignLabel.textContent = L.alignment;
|
|
98
99
|
const alignRow = createElement('div', { class: 'an-align-row' });
|
|
99
100
|
const alignOptions = [
|
|
100
|
-
{ value: '', label:
|
|
101
|
-
{ value: 'left', label:
|
|
102
|
-
{ value: 'center', label:
|
|
103
|
-
{ value: 'right', label:
|
|
101
|
+
{ value: '', label: L.alignNone },
|
|
102
|
+
{ value: 'left', label: L.alignLeft },
|
|
103
|
+
{ value: 'center', label: L.alignCenter },
|
|
104
|
+
{ value: 'right', label: L.alignRight },
|
|
104
105
|
];
|
|
105
106
|
alignOptions.forEach(({ value, label }) => {
|
|
106
107
|
const radioId = `an-align-${value || 'none'}`;
|
|
@@ -114,7 +115,7 @@ export class ImageDialog {
|
|
|
114
115
|
box.append(alignLabel, alignRow);
|
|
115
116
|
if (this.options.allowImageUpload !== false) {
|
|
116
117
|
const fileLabel = createElement('label', { class: 'an-label' });
|
|
117
|
-
fileLabel.textContent =
|
|
118
|
+
fileLabel.textContent = L.uploadLabel;
|
|
118
119
|
const fileInput = createElement('input', {
|
|
119
120
|
type: 'file',
|
|
120
121
|
class: 'an-input',
|
|
@@ -132,9 +133,9 @@ export class ImageDialog {
|
|
|
132
133
|
// Buttons
|
|
133
134
|
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
134
135
|
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
135
|
-
insertBtn.textContent =
|
|
136
|
+
insertBtn.textContent = L.insertBtn;
|
|
136
137
|
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
137
|
-
cancelBtn.textContent =
|
|
138
|
+
cancelBtn.textContent = L.cancelBtn;
|
|
138
139
|
btnRow.appendChild(insertBtn);
|
|
139
140
|
btnRow.appendChild(cancelBtn);
|
|
140
141
|
|
|
@@ -166,7 +167,7 @@ export class ImageDialog {
|
|
|
166
167
|
'image/webp', 'image/svg+xml', 'image/avif',
|
|
167
168
|
]);
|
|
168
169
|
if (!SUPPORTED.has(file.type)) {
|
|
169
|
-
const message =
|
|
170
|
+
const message = this.context.locale.errors.imageFormat(file.type);
|
|
170
171
|
if (this._fileHint) this._fileHint.textContent = message;
|
|
171
172
|
this.context.triggerEvent('imageError', { file, message });
|
|
172
173
|
this._fileInput.value = '';
|
|
@@ -177,7 +178,7 @@ export class ImageDialog {
|
|
|
177
178
|
// Validate file size (max 5 MB by default)
|
|
178
179
|
const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
|
|
179
180
|
if (file.size > maxSize) {
|
|
180
|
-
const message =
|
|
181
|
+
const message = this.context.locale.errors.imageSize(this.options.maxImageSize || 5);
|
|
181
182
|
if (this._fileHint) this._fileHint.textContent = message;
|
|
182
183
|
console.warn('[AutumnNote] ImageDialog:', message);
|
|
183
184
|
this.context.triggerEvent('imageError', { file, message });
|
|
@@ -75,24 +75,25 @@ export class ImageTooltip {
|
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
|
|
77
77
|
_buildTooltip() {
|
|
78
|
+
const L = this.context.locale.tooltips.image;
|
|
78
79
|
const el = createElement('div', {
|
|
79
80
|
class: 'an-link-tooltip an-image-tooltip',
|
|
80
81
|
role: 'toolbar',
|
|
81
|
-
'aria-label':
|
|
82
|
+
'aria-label': L.ariaLabel,
|
|
82
83
|
});
|
|
83
84
|
el.style.display = 'none';
|
|
84
85
|
|
|
85
86
|
// Label showing "Image"
|
|
86
87
|
this._label = createElement('span', { class: 'an-link-tooltip-url' });
|
|
87
|
-
this._label.textContent =
|
|
88
|
+
this._label.textContent = L.label;
|
|
88
89
|
el.appendChild(this._label);
|
|
89
90
|
|
|
90
91
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
91
92
|
|
|
92
|
-
this._floatLeftBtn = this._makeBtn(ICONS.floatLeft,
|
|
93
|
-
this._floatNoneBtn = this._makeBtn(ICONS.floatNone,
|
|
94
|
-
this._alignCenterBtn = this._makeBtn(ICONS.alignCenter,
|
|
95
|
-
this._floatRightBtn = this._makeBtn(ICONS.floatRight,
|
|
93
|
+
this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, L.floatLeft, () => this._setFloat('left'));
|
|
94
|
+
this._floatNoneBtn = this._makeBtn(ICONS.floatNone, L.noFloat, () => this._setFloat(''));
|
|
95
|
+
this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, L.alignCenter, () => this._setCenter());
|
|
96
|
+
this._floatRightBtn = this._makeBtn(ICONS.floatRight, L.floatRight, () => this._setFloat('right'));
|
|
96
97
|
|
|
97
98
|
el.appendChild(this._floatLeftBtn);
|
|
98
99
|
el.appendChild(this._floatNoneBtn);
|
|
@@ -101,28 +102,28 @@ export class ImageTooltip {
|
|
|
101
102
|
|
|
102
103
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
103
104
|
|
|
104
|
-
this._originalBtn = this._makeBtn(ICONS.originalSize,
|
|
105
|
+
this._originalBtn = this._makeBtn(ICONS.originalSize, L.originalSize, () => this._resetSize());
|
|
105
106
|
|
|
106
107
|
el.appendChild(this._originalBtn);
|
|
107
108
|
|
|
108
109
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
109
110
|
|
|
110
|
-
el.appendChild(this._makeBtn(ICONS.rotateLeft,
|
|
111
|
-
el.appendChild(this._makeBtn(ICONS.rotateRight,
|
|
111
|
+
el.appendChild(this._makeBtn(ICONS.rotateLeft, L.rotateLeft, () => this._rotate(-90)));
|
|
112
|
+
el.appendChild(this._makeBtn(ICONS.rotateRight, L.rotateRight, () => this._rotate(90)));
|
|
112
113
|
|
|
113
114
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
114
115
|
|
|
115
|
-
this._cropBtn = this._makeBtn(ICONS.crop,
|
|
116
|
+
this._cropBtn = this._makeBtn(ICONS.crop, L.cropImage, () => this._crop());
|
|
116
117
|
el.appendChild(this._cropBtn);
|
|
117
118
|
|
|
118
119
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
119
120
|
|
|
120
|
-
this._captionBtn = this._makeBtn(ICONS.caption,
|
|
121
|
+
this._captionBtn = this._makeBtn(ICONS.caption, L.addCaption, () => this._toggleCaption());
|
|
121
122
|
el.appendChild(this._captionBtn);
|
|
122
123
|
|
|
123
124
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
124
125
|
|
|
125
|
-
this._deleteBtn = this._makeBtn(ICONS.deleteImg,
|
|
126
|
+
this._deleteBtn = this._makeBtn(ICONS.deleteImg, L.deleteImage, () => this._delete(), true);
|
|
126
127
|
|
|
127
128
|
el.appendChild(this._deleteBtn);
|
|
128
129
|
|
|
@@ -59,19 +59,20 @@ export class LinkDialog {
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
61
|
_buildDialog() {
|
|
62
|
-
const
|
|
62
|
+
const L = this.context.locale.linkDialog;
|
|
63
|
+
const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': L.ariaLabel });
|
|
63
64
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
64
65
|
|
|
65
66
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
66
|
-
title.textContent =
|
|
67
|
+
title.textContent = L.title;
|
|
67
68
|
|
|
68
69
|
// URL field
|
|
69
70
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
70
|
-
urlLabel.textContent =
|
|
71
|
+
urlLabel.textContent = L.url;
|
|
71
72
|
const urlInput = createElement('input', {
|
|
72
73
|
type: 'url',
|
|
73
74
|
class: 'an-input',
|
|
74
|
-
placeholder:
|
|
75
|
+
placeholder: L.urlPlaceholder,
|
|
75
76
|
id: 'an-link-url',
|
|
76
77
|
name: 'url',
|
|
77
78
|
autocomplete: 'off',
|
|
@@ -80,11 +81,11 @@ export class LinkDialog {
|
|
|
80
81
|
|
|
81
82
|
// Text field
|
|
82
83
|
const textLabel = createElement('label', { class: 'an-label' });
|
|
83
|
-
textLabel.textContent =
|
|
84
|
+
textLabel.textContent = L.displayText;
|
|
84
85
|
const textInput = createElement('input', {
|
|
85
86
|
type: 'text',
|
|
86
87
|
class: 'an-input',
|
|
87
|
-
placeholder:
|
|
88
|
+
placeholder: L.textPlaceholder,
|
|
88
89
|
id: 'an-link-text',
|
|
89
90
|
name: 'linkText',
|
|
90
91
|
autocomplete: 'off',
|
|
@@ -100,14 +101,14 @@ export class LinkDialog {
|
|
|
100
101
|
});
|
|
101
102
|
this._tabCheckbox = tabCheckbox;
|
|
102
103
|
tabLabel.appendChild(tabCheckbox);
|
|
103
|
-
tabLabel.appendChild(document.createTextNode('
|
|
104
|
+
tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
|
|
104
105
|
|
|
105
106
|
// Buttons
|
|
106
107
|
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
107
108
|
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
108
|
-
insertBtn.textContent =
|
|
109
|
+
insertBtn.textContent = L.insertBtn;
|
|
109
110
|
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
110
|
-
cancelBtn.textContent =
|
|
111
|
+
cancelBtn.textContent = L.cancelBtn;
|
|
111
112
|
btnRow.appendChild(insertBtn);
|
|
112
113
|
btnRow.appendChild(cancelBtn);
|
|
113
114
|
|
|
@@ -63,7 +63,8 @@ export class LinkTooltip {
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
65
|
_buildTooltip() {
|
|
66
|
-
const
|
|
66
|
+
const L = this.context.locale.tooltips.link;
|
|
67
|
+
const el = createElement('div', { class: 'an-link-tooltip', role: 'toolbar', 'aria-label': L.ariaLabel });
|
|
67
68
|
el.style.display = 'none';
|
|
68
69
|
|
|
69
70
|
// URL preview text (truncated)
|
|
@@ -74,10 +75,10 @@ export class LinkTooltip {
|
|
|
74
75
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
75
76
|
|
|
76
77
|
// Action buttons
|
|
77
|
-
this._openBtn = this._makeBtn(ICONS.open,
|
|
78
|
-
this._copyBtn = this._makeBtn(ICONS.copy,
|
|
79
|
-
this._editBtn = this._makeBtn(ICONS.edit,
|
|
80
|
-
this._unlinkBtn = this._makeBtn(ICONS.unlink,
|
|
78
|
+
this._openBtn = this._makeBtn(ICONS.open, L.openLink, () => this._openLink());
|
|
79
|
+
this._copyBtn = this._makeBtn(ICONS.copy, L.copyUrl, () => this._copyLink());
|
|
80
|
+
this._editBtn = this._makeBtn(ICONS.edit, L.editLink, () => this._editLink());
|
|
81
|
+
this._unlinkBtn = this._makeBtn(ICONS.unlink, L.removeLink, () => this._unlink());
|
|
81
82
|
|
|
82
83
|
el.appendChild(this._openBtn);
|
|
83
84
|
el.appendChild(this._copyBtn);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownShortcuts.js — Convert Markdown-style syntax typed directly in the
|
|
3
|
+
* editor into rich HTML elements (input rules / auto-format).
|
|
4
|
+
*
|
|
5
|
+
* Activated when `markdownShortcuts: true` (the default).
|
|
6
|
+
*
|
|
7
|
+
* Block rules — triggered by Space or Enter at the start of a line:
|
|
8
|
+
* #[#[#]]· → H1 / H2 / H3
|
|
9
|
+
* >· → blockquote
|
|
10
|
+
* -· or *· → unordered list item
|
|
11
|
+
* 1.· → ordered list item
|
|
12
|
+
* [ ]· → checklist item
|
|
13
|
+
* --- → horizontal rule (on Enter)
|
|
14
|
+
* ``` → code block (on Enter)
|
|
15
|
+
*
|
|
16
|
+
* Inline rules — triggered when the closing marker is typed:
|
|
17
|
+
* **text** → <strong>
|
|
18
|
+
* *text* → <em>
|
|
19
|
+
* ~~text~~ → <s>
|
|
20
|
+
* `code` → <code>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { on } from '../core/dom.js';
|
|
24
|
+
|
|
25
|
+
export class MarkdownShortcuts {
|
|
26
|
+
/** @param {import('../Context.js').Context} context */
|
|
27
|
+
constructor(context) {
|
|
28
|
+
this.context = context;
|
|
29
|
+
this.options = context.options;
|
|
30
|
+
this._disposers = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
initialize() {
|
|
34
|
+
if (!this.options.markdownShortcuts) return this;
|
|
35
|
+
const editable = this.context.layoutInfo.editable;
|
|
36
|
+
const d1 = on(editable, 'keydown', /** @param {KeyboardEvent} e */ (e) => this._onKeydown(e));
|
|
37
|
+
const d2 = on(editable, 'input', () => this._onInput());
|
|
38
|
+
this._disposers.push(d1, d2);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
destroy() {
|
|
43
|
+
this._disposers.forEach((d) => d());
|
|
44
|
+
this._disposers = [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Block rules (Space / Enter)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
_onKeydown(e) {
|
|
52
|
+
if (e.key === ' ') {
|
|
53
|
+
if (this._applyBlockRule()) e.preventDefault();
|
|
54
|
+
} else if (e.key === 'Enter') {
|
|
55
|
+
if (this._applyEnterRule()) e.preventDefault();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the plain text of the line the cursor is on, up to the cursor.
|
|
61
|
+
* @returns {{ text: string, range: Range, lineNode: Node } | null}
|
|
62
|
+
*/
|
|
63
|
+
_getLineContext() {
|
|
64
|
+
const sel = window.getSelection();
|
|
65
|
+
if (!sel || !sel.rangeCount) return null;
|
|
66
|
+
const range = sel.getRangeAt(0);
|
|
67
|
+
if (!range.collapsed) return null;
|
|
68
|
+
|
|
69
|
+
const editable = this.context.layoutInfo.editable;
|
|
70
|
+
if (!editable.contains(range.startContainer)) return null;
|
|
71
|
+
|
|
72
|
+
// Walk up to find the block-level ancestor inside the editable
|
|
73
|
+
let node = range.startContainer;
|
|
74
|
+
while (node && node !== editable && !this._isBlock(node)) {
|
|
75
|
+
node = node.parentNode;
|
|
76
|
+
}
|
|
77
|
+
if (!node || node === editable) node = range.startContainer;
|
|
78
|
+
|
|
79
|
+
// Collect all text content before the cursor within that block
|
|
80
|
+
const tmpRange = document.createRange();
|
|
81
|
+
tmpRange.setStart(node, 0);
|
|
82
|
+
tmpRange.setEnd(range.startContainer, range.startOffset);
|
|
83
|
+
const text = tmpRange.toString();
|
|
84
|
+
|
|
85
|
+
return { text, range, lineNode: node };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_isBlock(node) {
|
|
89
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
90
|
+
const display = window.getComputedStyle(node).display;
|
|
91
|
+
return display === 'block' || display === 'list-item' || display === 'table-cell';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Applies block rule on Space key. Returns true if a rule fired. */
|
|
95
|
+
_applyBlockRule() {
|
|
96
|
+
const ctx = this._getLineContext();
|
|
97
|
+
if (!ctx) return false;
|
|
98
|
+
const { text } = ctx;
|
|
99
|
+
|
|
100
|
+
const blockPatterns = [
|
|
101
|
+
{ re: /^(#{1,3})$/, handler: (m) => this._convertToHeading(m[1].length) },
|
|
102
|
+
{ re: /^>$/, handler: () => this._convertToBlockquote() },
|
|
103
|
+
{ re: /^[-*]$/, handler: () => this._convertToList('ul') },
|
|
104
|
+
{ re: /^1\.$/, handler: () => this._convertToList('ol') },
|
|
105
|
+
{ re: /^\[ \]$/, handler: () => this._convertToChecklist() },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const { re, handler } of blockPatterns) {
|
|
109
|
+
const m = text.match(re);
|
|
110
|
+
if (m) {
|
|
111
|
+
handler(m);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Applies block rule on Enter key (---, ```). Returns true if a rule fired. */
|
|
119
|
+
_applyEnterRule() {
|
|
120
|
+
const ctx = this._getLineContext();
|
|
121
|
+
if (!ctx) return false;
|
|
122
|
+
const { text } = ctx;
|
|
123
|
+
|
|
124
|
+
if (/^-{3,}$/.test(text)) {
|
|
125
|
+
this._convertToHr();
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (/^`{3}/.test(text)) {
|
|
129
|
+
this._convertToCodeBlock();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Block converters
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
_selectLineAndDelete() {
|
|
140
|
+
const sel = window.getSelection();
|
|
141
|
+
if (!sel || !sel.rangeCount) return;
|
|
142
|
+
const range = sel.getRangeAt(0);
|
|
143
|
+
// Select from the start of the block to the cursor and delete
|
|
144
|
+
const startRange = document.createRange();
|
|
145
|
+
startRange.setStart(range.startContainer.parentNode || range.startContainer, 0);
|
|
146
|
+
startRange.setEnd(range.startContainer, range.startOffset);
|
|
147
|
+
startRange.deleteContents();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_convertToHeading(level) {
|
|
151
|
+
this._selectLineAndDelete();
|
|
152
|
+
document.execCommand('formatBlock', false, `h${level}`);
|
|
153
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_convertToBlockquote() {
|
|
157
|
+
this._selectLineAndDelete();
|
|
158
|
+
document.execCommand('formatBlock', false, 'blockquote');
|
|
159
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_convertToList(type) {
|
|
163
|
+
this._selectLineAndDelete();
|
|
164
|
+
document.execCommand(type === 'ul' ? 'insertUnorderedList' : 'insertOrderedList');
|
|
165
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_convertToChecklist() {
|
|
169
|
+
this._selectLineAndDelete();
|
|
170
|
+
this.context.invoke('editor.checklist');
|
|
171
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_convertToHr() {
|
|
175
|
+
this._selectLineAndDelete();
|
|
176
|
+
this.context.invoke('editor.insertHR');
|
|
177
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_convertToCodeBlock() {
|
|
181
|
+
this._selectLineAndDelete();
|
|
182
|
+
document.execCommand('formatBlock', false, 'pre');
|
|
183
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Inline rules (input event)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
_onInput() {
|
|
191
|
+
const sel = window.getSelection();
|
|
192
|
+
if (!sel || !sel.rangeCount) return;
|
|
193
|
+
const range = sel.getRangeAt(0);
|
|
194
|
+
if (!range.collapsed) return;
|
|
195
|
+
|
|
196
|
+
const editable = this.context.layoutInfo.editable;
|
|
197
|
+
if (!editable.contains(range.startContainer)) return;
|
|
198
|
+
|
|
199
|
+
const node = range.startContainer;
|
|
200
|
+
if (node.nodeType !== Node.TEXT_NODE) return;
|
|
201
|
+
|
|
202
|
+
const text = node.textContent;
|
|
203
|
+
const offset = range.startOffset;
|
|
204
|
+
|
|
205
|
+
const inlineRules = [
|
|
206
|
+
// **bold**
|
|
207
|
+
{ re: /\*\*(.+?)\*\*$/, tag: 'strong' },
|
|
208
|
+
// *italic* (not part of **)
|
|
209
|
+
{ re: /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)$/, tag: 'em' },
|
|
210
|
+
// ~~strike~~
|
|
211
|
+
{ re: /~~(.+?)~~$/, tag: 's' },
|
|
212
|
+
// `code`
|
|
213
|
+
{ re: /`([^`]+)`$/, tag: 'code' },
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const upToCursor = text.slice(0, offset);
|
|
217
|
+
for (const { re, tag } of inlineRules) {
|
|
218
|
+
const m = upToCursor.match(re);
|
|
219
|
+
if (!m) continue;
|
|
220
|
+
|
|
221
|
+
const matchStart = upToCursor.length - m[0].length;
|
|
222
|
+
const matchEnd = offset;
|
|
223
|
+
const innerText = m[1];
|
|
224
|
+
|
|
225
|
+
// Replace matched text with formatted element
|
|
226
|
+
const before = text.slice(0, matchStart);
|
|
227
|
+
const after = text.slice(matchEnd);
|
|
228
|
+
|
|
229
|
+
const el = document.createElement(tag);
|
|
230
|
+
el.textContent = innerText;
|
|
231
|
+
|
|
232
|
+
// Rebuild the text node and insert the element
|
|
233
|
+
const beforeNode = document.createTextNode(before);
|
|
234
|
+
const afterNode = document.createTextNode('' + after);
|
|
235
|
+
|
|
236
|
+
const parent = node.parentNode;
|
|
237
|
+
parent.insertBefore(beforeNode, node);
|
|
238
|
+
parent.insertBefore(el, node);
|
|
239
|
+
parent.insertBefore(afterNode, node);
|
|
240
|
+
parent.removeChild(node);
|
|
241
|
+
|
|
242
|
+
// Place cursor after the element (after the ZWS)
|
|
243
|
+
const newRange = document.createRange();
|
|
244
|
+
newRange.setStart(afterNode, 1);
|
|
245
|
+
newRange.collapse(true);
|
|
246
|
+
sel.removeAllRanges();
|
|
247
|
+
sel.addRange(newRange);
|
|
248
|
+
|
|
249
|
+
this.context.triggerEvent('change', this.context.getHTML());
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|