autumnnote 2.0.0 → 2.2.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 (64) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +661 -798
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +20 -20
  7. package/dist/autumnnote.umd.js +20 -20
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/icon-data-V0Xqv-wX.js +255 -0
  10. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  11. package/package.json +13 -4
  12. package/types/index.d.ts +8 -0
  13. package/src/js/Context.js +0 -854
  14. package/src/js/core/detectLang.js +0 -98
  15. package/src/js/core/dom.js +0 -372
  16. package/src/js/core/env.js +0 -25
  17. package/src/js/core/key.js +0 -66
  18. package/src/js/core/lists.js +0 -121
  19. package/src/js/core/markdown.js +0 -695
  20. package/src/js/core/range.js +0 -194
  21. package/src/js/core/sanitise.js +0 -231
  22. package/src/js/editing/History.js +0 -266
  23. package/src/js/editing/Style.js +0 -812
  24. package/src/js/editing/Table.js +0 -105
  25. package/src/js/editing/Typing.js +0 -397
  26. package/src/js/index.js +0 -193
  27. package/src/js/index.umd.js +0 -17
  28. package/src/js/module/AutoSaveRestore.js +0 -125
  29. package/src/js/module/BaseDialog.js +0 -133
  30. package/src/js/module/BaseMediaTooltip.js +0 -142
  31. package/src/js/module/BaseResizer.js +0 -312
  32. package/src/js/module/BubbleToolbar.js +0 -483
  33. package/src/js/module/Buttons.js +0 -399
  34. package/src/js/module/Clipboard.js +0 -579
  35. package/src/js/module/CodeTooltip.js +0 -493
  36. package/src/js/module/Codeview.js +0 -125
  37. package/src/js/module/ContextMenu.js +0 -621
  38. package/src/js/module/Editor.js +0 -747
  39. package/src/js/module/EmojiDialog.js +0 -254
  40. package/src/js/module/FindReplace.js +0 -512
  41. package/src/js/module/Fullscreen.js +0 -80
  42. package/src/js/module/IconDialog.js +0 -618
  43. package/src/js/module/ImageCropOverlay.js +0 -586
  44. package/src/js/module/ImageDialog.js +0 -193
  45. package/src/js/module/ImageResizer.js +0 -42
  46. package/src/js/module/ImageTooltip.js +0 -285
  47. package/src/js/module/LinkDialog.js +0 -145
  48. package/src/js/module/LinkTooltip.js +0 -250
  49. package/src/js/module/MarkdownShortcuts.js +0 -250
  50. package/src/js/module/Mention.js +0 -365
  51. package/src/js/module/Placeholder.js +0 -51
  52. package/src/js/module/ShortcutsDialog.js +0 -111
  53. package/src/js/module/SlashMenu.js +0 -376
  54. package/src/js/module/Statusbar.js +0 -246
  55. package/src/js/module/TableTooltip.js +0 -1521
  56. package/src/js/module/Toolbar.js +0 -750
  57. package/src/js/module/VideoDialog.js +0 -193
  58. package/src/js/module/VideoResizer.js +0 -66
  59. package/src/js/module/VideoTooltip.js +0 -248
  60. package/src/js/module/emoji-data.js +0 -496
  61. package/src/js/renderer.js +0 -120
  62. package/src/js/settings.js +0 -214
  63. package/src/styles/_variables.scss +0 -48
  64. package/src/styles/autumnnote.scss +0 -2866
@@ -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
- }