autumnnote 2.1.0 → 2.3.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.
Files changed (65) hide show
  1. package/README.md +74 -6
  2. package/dist/autumnnote.cjs +27 -22
  3. package/dist/autumnnote.es.js +474 -591
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +27 -22
  6. package/dist/autumnnote.umd.js +27 -22
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
@@ -1,193 +0,0 @@
1
- /**
2
- * ImageDialog.js - Dialog for inserting images (by URL or file upload)
3
- * Inspired by Summernote's ImageDialog — rewritten without jQuery
4
- */
5
-
6
- import { createElement, on } from '../core/dom.js';
7
- import { BaseDialog } from './BaseDialog.js';
8
-
9
- const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
10
-
11
- export class ImageDialog extends BaseDialog {
12
- // ---------------------------------------------------------------------------
13
- // Public API
14
- // ---------------------------------------------------------------------------
15
-
16
- /**
17
- * @param {{ beforeInsert?: () => void }} [hooks]
18
- */
19
- show({ beforeInsert } = {}) {
20
- this._beforeInsert = typeof beforeInsert === 'function' ? beforeInsert : null;
21
- this._saveRange();
22
- this._urlInput.value = '';
23
- this._altInput.value = '';
24
- if (this._fileInput) this._fileInput.value = '';
25
- this._open();
26
- }
27
-
28
- // ---------------------------------------------------------------------------
29
- // Build dialog
30
- // ---------------------------------------------------------------------------
31
-
32
- _buildDialog() {
33
- const L = this.context.locale.imageDialog;
34
- const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
35
-
36
- // URL field
37
- const urlLabel = createElement('label', { class: 'an-label' });
38
- urlLabel.textContent = L.imageUrl;
39
- const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
40
- type: 'url',
41
- class: 'an-input',
42
- placeholder: L.urlPlaceholder,
43
- autocomplete: 'off',
44
- }));
45
- this._urlInput = urlInput;
46
- this._firstInput = urlInput;
47
-
48
- // Alt text
49
- const altLabel = createElement('label', { class: 'an-label' });
50
- altLabel.textContent = L.altText;
51
- const altInput = /** @type {HTMLInputElement} */ (createElement('input', {
52
- type: 'text',
53
- class: 'an-input',
54
- placeholder: L.altPlaceholder,
55
- autocomplete: 'off',
56
- }));
57
- this._altInput = altInput;
58
-
59
- box.append(urlLabel, urlInput, altLabel, altInput);
60
-
61
- // Alignment
62
- const alignLabel = createElement('label', { class: 'an-label' });
63
- alignLabel.textContent = L.alignment;
64
- const alignRow = createElement('div', { class: 'an-align-row' });
65
- const alignOptions = [
66
- { value: '', label: L.alignNone },
67
- { value: 'left', label: L.alignLeft },
68
- { value: 'center', label: L.alignCenter },
69
- { value: 'right', label: L.alignRight },
70
- ];
71
- alignOptions.forEach(({ value, label }) => {
72
- const radioId = `an-align-${value || 'none'}`;
73
- const radio = /** @type {HTMLInputElement} */ (createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value }));
74
- if (value === '') radio.checked = true;
75
- const lbl = createElement('label', { for: radioId, class: 'an-align-label' });
76
- lbl.textContent = label;
77
- alignRow.append(radio, lbl);
78
- });
79
- this._alignRow = alignRow;
80
- box.append(alignLabel, alignRow);
81
-
82
- if (this.options.allowImageUpload !== false) {
83
- const fileLabel = createElement('label', { class: 'an-label' });
84
- fileLabel.textContent = L.uploadLabel;
85
- const fileInput = /** @type {HTMLInputElement} */ (createElement('input', {
86
- type: 'file',
87
- class: 'an-input',
88
- accept: 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/avif',
89
- }));
90
- this._fileInput = fileInput;
91
- // Hint line shown below the file input for format errors
92
- const fileHint = createElement('p', { class: 'an-dialog-hint' });
93
- this._fileHint = fileHint;
94
- const d = on(fileInput, 'change', () => this._onFileChange());
95
- this._disposers.push(d);
96
- box.append(fileLabel, fileInput, fileHint);
97
- }
98
-
99
- const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
100
- box.append(btnRow);
101
-
102
- const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
103
- const d5 = on(altInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
104
- this._disposers.push(d4, d5);
105
-
106
- return overlay;
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // Actions
111
- // ---------------------------------------------------------------------------
112
-
113
- _onFileChange() {
114
- if (this.context._alive === false) return;
115
- const file = this._fileInput?.files?.[0];
116
- if (!file?.type?.startsWith('image/')) return;
117
-
118
- // C2: Only allow web-displayable formats. TIFF, BMP, RAW and similar
119
- // formats are not rendered by browsers — reject them with a clear message.
120
- const SUPPORTED = new Set([
121
- 'image/jpeg', 'image/png', 'image/gif',
122
- 'image/webp', 'image/svg+xml', 'image/avif',
123
- ]);
124
- if (!SUPPORTED.has(file.type)) {
125
- const message = this.context.locale.errors.imageFormat(file.type);
126
- if (this._fileHint) this._fileHint.textContent = message;
127
- this.context.triggerEvent('imageError', { file, message });
128
- this._fileInput.value = '';
129
- return;
130
- }
131
- if (this._fileHint) this._fileHint.textContent = '';
132
-
133
- // Validate file size (max 5 MB by default)
134
- const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
135
- if (file.size > maxSize) {
136
- const message = this.context.locale.errors.imageSize(this.options.maxImageSize || 5);
137
- if (this._fileHint) this._fileHint.textContent = message;
138
- console.warn('[AutumnNote] ImageDialog:', message);
139
- this.context.triggerEvent('imageError', { file, message });
140
- this._fileInput.value = '';
141
- return;
142
- }
143
-
144
- // Resize/compress via Clipboard's shared canvas pipeline so a file chosen
145
- // through this dialog gets the same treatment as a pasted/dropped image
146
- // instead of embedding the original (potentially multi-MB) file untouched.
147
- this.context.invoke('clipboard.compressAndRegister', file).then((blobUrl) => {
148
- if (this.context._alive === false) return;
149
- this._urlInput.value = blobUrl;
150
- }).catch((err) => {
151
- if (this.context._alive === false) return;
152
- // Compression failed (e.g. canvas unavailable) — fall back to embedding
153
- // the original file directly.
154
- console.warn('[AutumnNote] ImageDialog: image compression failed, using original file.', err);
155
- const reader = new FileReader();
156
- reader.onload = (e) => {
157
- this._urlInput.value = /** @type {string} */ (e.target.result);
158
- };
159
- reader.readAsDataURL(file);
160
- });
161
- }
162
-
163
- _onInsert() {
164
- const src = this._urlInput.value.trim();
165
- const alt = this._altInput.value.trim();
166
- const alignRadio = this._alignRow?.querySelector('input[name="an-img-align"]:checked');
167
- const align = alignRadio ? /** @type {HTMLInputElement} */ (alignRadio).value : '';
168
-
169
- if (!src) {
170
- this._urlInput.focus();
171
- return;
172
- }
173
-
174
- if (this._savedRange) {
175
- this._savedRange.select();
176
- }
177
-
178
- // Let callers make a deferred DOM change only after the user commits the
179
- // dialog. SlashMenu uses this to keep "/image" intact when the dialog is
180
- // cancelled and remove it atomically immediately before image insertion.
181
- const beforeInsert = this._beforeInsert;
182
- this._beforeInsert = null;
183
- beforeInsert?.();
184
-
185
- this.context.invoke('editor.insertImage', src, alt, align);
186
- this._close();
187
- }
188
-
189
- _close() {
190
- this._beforeInsert = null;
191
- super._close();
192
- }
193
- }
@@ -1,42 +0,0 @@
1
- // ImageResizer.js - Interactive resize handles for selected images in the editor
2
- import { BaseResizer } from './BaseResizer.js';
3
-
4
- export class ImageResizer extends BaseResizer {
5
- get _overlayClass() { return 'an-image-resizer'; }
6
- get _selectedClass() { return 'an-image-selected'; }
7
-
8
- /** @param {EventTarget|null} el @returns {HTMLImageElement|null} */
9
- _findTarget(el) {
10
- return /** @type {HTMLImageElement|null} */ (
11
- el instanceof Element ? el.closest('img') : null
12
- );
13
- }
14
-
15
- /** @param {HTMLImageElement} img */
16
- _getStartSize(img) {
17
- return {
18
- w: img.offsetWidth || img.naturalWidth || 100,
19
- h: img.offsetHeight || img.naturalHeight || 100,
20
- };
21
- }
22
-
23
- _getMinSize() {
24
- const min = this.context.options?.minImageSize ?? 20;
25
- return { minW: min, minH: min };
26
- }
27
-
28
- /** @param {HTMLImageElement} img @param {number} w @param {number} h */
29
- _applySize(img, w, h) {
30
- img.style.width = `${w}px`;
31
- img.style.height = `${h}px`;
32
- }
33
-
34
- // ---------------------------------------------------------------------------
35
- // Public API used by other modules
36
- // ---------------------------------------------------------------------------
37
-
38
- /** @returns {HTMLImageElement|null} */
39
- getActiveImage() {
40
- return /** @type {HTMLImageElement|null} */ (this._active);
41
- }
42
- }
@@ -1,285 +0,0 @@
1
- // ImageTooltip.js - Hover tooltip for images inside the editor
2
- // Displays a horizontal action bar below (or above) the selected image,
3
- // similar in appearance and interaction to LinkTooltip.
4
- import { createElement, on } from '../core/dom.js';
5
- import { BaseMediaTooltip } from './BaseMediaTooltip.js';
6
-
7
- const ICONS = {
8
- floatLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="8" height="8" rx="1"/><line x1="12" y1="6" x2="22" y2="6"/><line x1="12" y1="9" x2="22" y2="9"/><line x1="12" y1="12" x2="22" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
9
- floatRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="4" width="8" height="8" rx="1"/><line x1="2" y1="6" x2="12" y2="6"/><line x1="2" y1="9" x2="12" y2="9"/><line x1="2" y1="12" x2="12" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
10
- floatNone: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="1"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="3" y1="19" x2="17" y2="19"/></svg>`,
11
- alignCenter: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="4" width="10" height="8" rx="1"/><line x1="3" y1="16" x2="21" y2="16"/><line x1="6" y1="20" x2="18" y2="20"/></svg>`,
12
- originalSize:`<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
13
- deleteImg: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
14
- caption: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/></svg>`,
15
- rotateLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><polyline points="3 3 3 8 8 8"/></svg>`,
16
- rotateRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><polyline points="21 3 21 8 16 8"/></svg>`,
17
- crop: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 2 6 18 22 18"/><polyline points="2 6 18 6 18 22"/></svg>`,
18
- };
19
-
20
- export class ImageTooltip extends BaseMediaTooltip {
21
- initialize() {
22
- this._el = this._buildTooltip();
23
- document.body.appendChild(this._el);
24
-
25
- const editable = this.context.layoutInfo.editable;
26
-
27
- this._disposers.push(
28
- on(editable, 'mouseover', (e) => {
29
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
30
- const img = /** @type {Element} */ (e.target)?.closest('img');
31
- // Skip when the image is inside a link — LinkTooltip takes priority there
32
- if (img && editable.contains(img) && !img.closest('a[href]')) {
33
- this._scheduleShow(img);
34
- }
35
- }, { passive: true }),
36
- on(editable, 'mouseout', (e) => {
37
- const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
38
- if (!to || (!editable.contains(to) && !this._el.contains(to))) {
39
- this._scheduleHide();
40
- }
41
- }, { passive: true }),
42
- // Hide when image is deselected by clicking elsewhere
43
- on(document, 'click', (e) => {
44
- const et = /** @type {Node} */ (e.target);
45
- if (this._active && !this._active.contains(et) && !this._el.contains(et)) {
46
- this._hide();
47
- }
48
- }),
49
- // Hide when the page scrolls or resizes — the tooltip position becomes stale
50
- on(globalThis, 'scroll', () => this._hide(), { passive: true }),
51
- on(globalThis, 'resize', () => this._hide(), { passive: true }),
52
- );
53
-
54
- return this;
55
- }
56
-
57
- destroy() {
58
- this._clearTimers();
59
- this._disposers.forEach((d) => d());
60
- this._disposers = [];
61
- this._el?.remove();
62
- this._el = null;
63
- }
64
-
65
- // ---------------------------------------------------------------------------
66
- // Build
67
- // ---------------------------------------------------------------------------
68
-
69
- _buildTooltip() {
70
- const L = this.context.locale.tooltips.image;
71
- const el = createElement('div', {
72
- class: 'an-link-tooltip an-image-tooltip',
73
- role: 'toolbar',
74
- 'aria-label': L.ariaLabel,
75
- });
76
- el.style.display = 'none';
77
-
78
- // Label showing "Image"
79
- this._label = createElement('span', { class: 'an-link-tooltip-url' });
80
- this._label.textContent = L.label;
81
- el.appendChild(this._label);
82
-
83
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
84
-
85
- this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, L.floatLeft, () => this._setFloat('left'));
86
- this._floatNoneBtn = this._makeBtn(ICONS.floatNone, L.noFloat, () => this._setFloat(''));
87
- this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, L.alignCenter, () => this._setCenter());
88
- this._floatRightBtn = this._makeBtn(ICONS.floatRight, L.floatRight, () => this._setFloat('right'));
89
-
90
- el.appendChild(this._floatLeftBtn);
91
- el.appendChild(this._floatNoneBtn);
92
- el.appendChild(this._alignCenterBtn);
93
- el.appendChild(this._floatRightBtn);
94
-
95
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
96
-
97
- this._originalBtn = this._makeBtn(ICONS.originalSize, L.originalSize, () => this._resetSize());
98
-
99
- el.appendChild(this._originalBtn);
100
-
101
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
102
-
103
- el.appendChild(this._makeBtn(ICONS.rotateLeft, L.rotateLeft, () => this._rotate(-90)));
104
- el.appendChild(this._makeBtn(ICONS.rotateRight, L.rotateRight, () => this._rotate(90)));
105
-
106
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
107
-
108
- this._cropBtn = this._makeBtn(ICONS.crop, L.cropImage, () => this._crop());
109
- el.appendChild(this._cropBtn);
110
-
111
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
112
-
113
- this._captionBtn = this._makeBtn(ICONS.caption, L.addCaption, () => this._toggleCaption());
114
- el.appendChild(this._captionBtn);
115
-
116
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
117
-
118
- this._deleteBtn = this._makeBtn(ICONS.deleteImg, L.deleteImage, () => this._delete(), true);
119
-
120
- el.appendChild(this._deleteBtn);
121
-
122
- // Keep tooltip alive while hovering it
123
- this._disposers.push(
124
- on(el, 'mouseenter', () => this._clearTimers()),
125
- on(el, 'mouseleave', () => this._scheduleHide()),
126
- );
127
-
128
- return el;
129
- }
130
-
131
- // ---------------------------------------------------------------------------
132
- // Actions
133
- // ---------------------------------------------------------------------------
134
-
135
- _setFloat(value) {
136
- const img = this._active;
137
- if (!img) return;
138
- const target = /** @type {HTMLElement} */ (img.closest('figure.an-figure') || img);
139
- target.style.float = value;
140
- target.style.display = '';
141
- // Clear explicit width set for centering so the figure is free to float
142
- if (target !== img) target.style.width = '';
143
- target.style.marginLeft = value === 'right' ? '12px' : '';
144
- target.style.marginRight = value === 'left' ? '12px' : '';
145
- this.context.invoke('editor.afterCommand');
146
- this.context.invoke('imageResizer.updateOverlay');
147
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
148
- }
149
-
150
- _setCenter() {
151
- const img = this._active;
152
- if (!img) return;
153
- const target = /** @type {HTMLElement} */ (img.closest('figure.an-figure') || img);
154
- target.style.float = '';
155
- target.style.display = 'block';
156
- // C1/C3: use fit-content so the block figure shrinks to the image width;
157
- // without an explicit width, margin:auto has no effect on a 100%-wide block.
158
- if (target !== img) target.style.width = 'fit-content';
159
- target.style.marginLeft = 'auto';
160
- target.style.marginRight = 'auto';
161
- this.context.invoke('editor.afterCommand');
162
- this.context.invoke('imageResizer.updateOverlay');
163
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
164
- }
165
-
166
- _resetSize() {
167
- const img = this._active;
168
- if (!img) return;
169
- img.style.width = '';
170
- img.style.height = '';
171
- this.context.invoke('editor.afterCommand');
172
- this.context.invoke('imageResizer.updateOverlay');
173
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
174
- }
175
-
176
- /**
177
- * Rotate the active image by `delta` degrees (±90).
178
- * Reads the existing rotate() value from the transform style so
179
- * repeated clicks accumulate correctly.
180
- * @param {number} delta
181
- */
182
- _rotate(delta) {
183
- const img = this._active;
184
- if (!img) return;
185
- // Parse current rotation angle from inline transform
186
- const current = img.style.transform || '';
187
- const match = /rotate\((-?[\d.]+)deg\)/.exec(current);
188
- const prev = match ? Number.parseFloat(match[1]) : 0;
189
- const next = (prev + delta + 360) % 360; // normalise to [0, 360)
190
- // Preserve any other transform functions (e.g. scale), replace only rotate()
191
- const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
192
- if (cleaned) {
193
- img.style.transform = `${cleaned} rotate(${next}deg)`;
194
- } else {
195
- img.style.transform = next === 0 ? '' : `rotate(${next}deg)`;
196
- }
197
- this.context.invoke('editor.afterCommand');
198
- this.context.invoke('imageResizer.updateOverlay');
199
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
200
- }
201
-
202
- _delete() {
203
- const img = this._active;
204
- if (!img) return;
205
- this._hide();
206
- this.context.invoke('imageResizer.deselect');
207
- const figure = img.closest('figure.an-figure');
208
- if (figure) {
209
- figure.remove();
210
- } else {
211
- img.remove();
212
- }
213
- this.context.invoke('editor.afterCommand');
214
- }
215
-
216
- _crop() {
217
- const img = this._active;
218
- if (!img) return;
219
- // Hide the tooltip while the crop overlay is active
220
- this._hide();
221
- // Delegate to imageCropOverlay module
222
- this.context.invoke('imageCropOverlay.open', img);
223
- }
224
-
225
- _toggleCaption() {
226
- const img = this._active;
227
- if (!img) return;
228
-
229
- // If already inside a figure, move focus to the figcaption
230
- const existing = img.closest('figure.an-figure');
231
- if (existing) {
232
- const cap = existing.querySelector('figcaption.an-figcaption');
233
- if (cap) {
234
- this._hide();
235
- const range = document.createRange();
236
- range.selectNodeContents(cap);
237
- const sel = globalThis.getSelection();
238
- if (sel) { sel.removeAllRanges(); sel.addRange(range); }
239
- }
240
- return;
241
- }
242
-
243
- // Wrap image in <figure><figcaption>
244
- const figure = document.createElement('figure');
245
- figure.className = 'an-figure';
246
-
247
- // Transfer existing alignment from <img> to <figure> (C4 fix)
248
- if (img.style.float) {
249
- figure.style.float = img.style.float;
250
- figure.style.marginLeft = img.style.marginLeft;
251
- figure.style.marginRight = img.style.marginRight;
252
- img.style.float = '';
253
- img.style.marginLeft = '';
254
- img.style.marginRight = '';
255
- } else if (img.style.display === 'block' && img.style.marginLeft === 'auto') {
256
- // C1/C3: use fit-content so the centered block figure tightly wraps the
257
- // image, making margin:auto actually centre it regardless of image width.
258
- figure.style.display = 'block';
259
- figure.style.width = 'fit-content';
260
- figure.style.marginLeft = 'auto';
261
- figure.style.marginRight = 'auto';
262
- img.style.display = '';
263
- img.style.marginLeft = '';
264
- img.style.marginRight = '';
265
- }
266
- const figcaption = document.createElement('figcaption');
267
- figcaption.className = 'an-figcaption';
268
- figcaption.textContent = 'Caption';
269
-
270
- img.parentNode.insertBefore(figure, img);
271
- figure.appendChild(img);
272
- figure.appendChild(figcaption);
273
-
274
- // Select caption text immediately
275
- const range = document.createRange();
276
- range.selectNodeContents(figcaption);
277
- const sel = globalThis.getSelection();
278
- if (sel) { sel.removeAllRanges(); sel.addRange(range); }
279
-
280
- this.context.invoke('editor.afterCommand');
281
- // Re-sync the resize overlay: wrapping img in <figure> changes its layout position.
282
- this.context.invoke('imageResizer.updateOverlay');
283
- this._hide();
284
- }
285
- }
@@ -1,145 +0,0 @@
1
- /**
2
- * LinkDialog.js - Dialog for inserting / editing hyperlinks
3
- * Inspired by Summernote's LinkDialog — rewritten without jQuery
4
- */
5
-
6
- import { createElement, on } from '../core/dom.js';
7
- import { BaseDialog } from './BaseDialog.js';
8
-
9
- const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
10
-
11
- export class LinkDialog extends BaseDialog {
12
- // ---------------------------------------------------------------------------
13
- // Public API
14
- // ---------------------------------------------------------------------------
15
-
16
- /**
17
- * Opens the link dialog.
18
- * Pre-fills with the currently selected link if present.
19
- */
20
- show() {
21
- this._saveRange();
22
- this._prefill();
23
- this._open();
24
- }
25
-
26
- // ---------------------------------------------------------------------------
27
- // Build dialog
28
- // ---------------------------------------------------------------------------
29
-
30
- _buildDialog() {
31
- const L = this.context.locale.linkDialog;
32
- const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
33
-
34
- // URL field
35
- const urlLabel = createElement('label', { class: 'an-label' });
36
- urlLabel.textContent = L.url;
37
- const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
38
- type: 'url',
39
- class: 'an-input',
40
- placeholder: L.urlPlaceholder,
41
- id: 'an-link-url',
42
- name: 'url',
43
- autocomplete: 'off',
44
- }));
45
- this._urlInput = urlInput;
46
- this._firstInput = urlInput;
47
-
48
- // Text field
49
- const textLabel = createElement('label', { class: 'an-label' });
50
- textLabel.textContent = L.displayText;
51
- const textInput = /** @type {HTMLInputElement} */ (createElement('input', {
52
- type: 'text',
53
- class: 'an-input',
54
- placeholder: L.textPlaceholder,
55
- id: 'an-link-text',
56
- name: 'linkText',
57
- autocomplete: 'off',
58
- }));
59
- this._textInput = textInput;
60
-
61
- // Open in new tab
62
- const tabLabel = createElement('label', { class: 'an-label an-label-inline' });
63
- const tabCheckbox = /** @type {HTMLInputElement} */ (createElement('input', {
64
- type: 'checkbox',
65
- id: 'an-link-newtab',
66
- name: 'openInNewTab',
67
- }));
68
- this._tabCheckbox = tabCheckbox;
69
- tabLabel.appendChild(tabCheckbox);
70
- tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
71
-
72
- const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
73
-
74
- box.append(urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
75
-
76
- const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
77
- const d5 = on(textInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
78
- this._disposers.push(d4, d5);
79
-
80
- return overlay;
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Actions
85
- // ---------------------------------------------------------------------------
86
-
87
- _prefill() {
88
- // Try to find currently selected anchor
89
- const sel = globalThis.getSelection();
90
- let anchor = null;
91
- if (sel?.rangeCount > 0) {
92
- let node = sel.getRangeAt(0).startContainer;
93
- while (node) {
94
- if (node.nodeName === 'A') { anchor = node; break; }
95
- node = node.parentNode;
96
- }
97
- }
98
- if (anchor) {
99
- this._urlInput.value = /** @type {Element} */ (anchor).getAttribute('href') || '';
100
- this._textInput.value = anchor.textContent || '';
101
- this._tabCheckbox.checked = /** @type {Element} */ (anchor).getAttribute('target') === '_blank';
102
- } else {
103
- this._urlInput.value = '';
104
- this._textInput.value = sel ? sel.toString() : '';
105
- this._tabCheckbox.checked = false;
106
- }
107
- }
108
-
109
- _onInsert() {
110
- let url = this._urlInput.value.trim();
111
- const text = this._textInput.value.trim();
112
- const newTab = this._tabCheckbox.checked;
113
-
114
- if (!url) {
115
- this._urlInput.focus();
116
- return;
117
- }
118
-
119
- // Auto-prefix with https:// if no protocol is present
120
- if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
121
- url = 'https://' + url;
122
- }
123
-
124
- // Block unsafe protocols
125
- try {
126
- const parsed = new URL(url);
127
- if (/^javascript:/i.test(parsed.protocol) || /^vbscript:/i.test(parsed.protocol) || /^data:/i.test(parsed.protocol)) {
128
- this._urlInput.focus();
129
- return;
130
- }
131
- } catch {
132
- // URL constructor failed — not a valid URL
133
- this._urlInput.focus();
134
- return;
135
- }
136
-
137
- // Restore selection then insert
138
- if (this._savedRange) {
139
- this._savedRange.select();
140
- }
141
-
142
- this.context.invoke('editor.insertLink', url, text, newTab);
143
- this._close();
144
- }
145
- }