autumnnote 1.0.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +874 -0
  3. package/dist/autumnnote.css +1 -0
  4. package/dist/autumnnote.es.js +5888 -0
  5. package/dist/autumnnote.es.js.map +1 -0
  6. package/dist/autumnnote.umd.js +74 -0
  7. package/dist/autumnnote.umd.js.map +1 -0
  8. package/package.json +55 -0
  9. package/src/js/Context.js +497 -0
  10. package/src/js/core/dom.js +315 -0
  11. package/src/js/core/env.js +25 -0
  12. package/src/js/core/func.js +153 -0
  13. package/src/js/core/key.js +66 -0
  14. package/src/js/core/lists.js +121 -0
  15. package/src/js/core/markdown.js +294 -0
  16. package/src/js/core/range.js +194 -0
  17. package/src/js/core/sanitise.js +78 -0
  18. package/src/js/editing/History.js +205 -0
  19. package/src/js/editing/Style.js +329 -0
  20. package/src/js/editing/Table.js +59 -0
  21. package/src/js/editing/Typing.js +142 -0
  22. package/src/js/index.js +126 -0
  23. package/src/js/module/Buttons.js +300 -0
  24. package/src/js/module/Clipboard.js +460 -0
  25. package/src/js/module/CodeTooltip.js +428 -0
  26. package/src/js/module/Codeview.js +122 -0
  27. package/src/js/module/ContextMenu.js +470 -0
  28. package/src/js/module/Editor.js +528 -0
  29. package/src/js/module/EmojiDialog.js +726 -0
  30. package/src/js/module/FindReplace.js +440 -0
  31. package/src/js/module/Fullscreen.js +80 -0
  32. package/src/js/module/IconDialog.js +620 -0
  33. package/src/js/module/ImageDialog.js +208 -0
  34. package/src/js/module/ImageResizer.js +216 -0
  35. package/src/js/module/ImageTooltip.js +286 -0
  36. package/src/js/module/LinkDialog.js +204 -0
  37. package/src/js/module/LinkTooltip.js +242 -0
  38. package/src/js/module/Placeholder.js +44 -0
  39. package/src/js/module/ShortcutsDialog.js +141 -0
  40. package/src/js/module/Statusbar.js +238 -0
  41. package/src/js/module/TableTooltip.js +568 -0
  42. package/src/js/module/Toolbar.js +562 -0
  43. package/src/js/module/VideoDialog.js +263 -0
  44. package/src/js/module/VideoResizer.js +227 -0
  45. package/src/js/module/VideoTooltip.js +252 -0
  46. package/src/js/renderer.js +107 -0
  47. package/src/js/settings.js +134 -0
  48. package/src/styles/_variables.scss +48 -0
  49. package/src/styles/autumnnote.scss +1740 -0
  50. package/types/index.d.ts +324 -0
@@ -0,0 +1,208 @@
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, trapFocus } from '../core/dom.js';
7
+ import { withSavedRange } from '../core/range.js';
8
+
9
+ export class ImageDialog {
10
+ /**
11
+ * @param {import('../Context.js').Context} context
12
+ */
13
+ constructor(context) {
14
+ this.context = context;
15
+ this.options = context.options;
16
+ /** @type {HTMLElement|null} */
17
+ this._dialog = null;
18
+ this._disposers = [];
19
+ this._savedRange = null;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Lifecycle
24
+ // ---------------------------------------------------------------------------
25
+
26
+ initialize() {
27
+ this._dialog = this._buildDialog();
28
+ document.body.appendChild(this._dialog);
29
+ return this;
30
+ }
31
+
32
+ destroy() {
33
+ this._disposers.forEach((d) => d());
34
+ this._disposers = [];
35
+ if (this._dialog && this._dialog.parentNode) {
36
+ this._dialog.parentNode.removeChild(this._dialog);
37
+ }
38
+ this._dialog = null;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Public API
43
+ // ---------------------------------------------------------------------------
44
+
45
+ show() {
46
+ withSavedRange((range) => {
47
+ this._savedRange = range;
48
+ });
49
+ this._urlInput.value = '';
50
+ this._altInput.value = '';
51
+ if (this._fileInput) this._fileInput.value = '';
52
+ this._open();
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Build dialog
57
+ // ---------------------------------------------------------------------------
58
+
59
+ _buildDialog() {
60
+ const overlay = createElement('div', {
61
+ class: 'an-dialog-overlay',
62
+ role: 'dialog',
63
+ 'aria-modal': 'true',
64
+ 'aria-label': 'Insert image',
65
+ });
66
+ const box = createElement('div', { class: 'an-dialog-box' });
67
+
68
+ const title = createElement('h3', { class: 'an-dialog-title' });
69
+ title.textContent = 'Insert Image';
70
+
71
+ // URL tab
72
+ const urlLabel = createElement('label', { class: 'an-label' });
73
+ urlLabel.textContent = 'Image URL';
74
+ const urlInput = createElement('input', {
75
+ type: 'url',
76
+ class: 'an-input',
77
+ placeholder: 'https://example.com/image.png',
78
+ autocomplete: 'off',
79
+ });
80
+ this._urlInput = urlInput;
81
+
82
+ // Alt text
83
+ const altLabel = createElement('label', { class: 'an-label' });
84
+ altLabel.textContent = 'Alt Text';
85
+ const altInput = createElement('input', {
86
+ type: 'text',
87
+ class: 'an-input',
88
+ placeholder: 'Describe the image',
89
+ autocomplete: 'off',
90
+ });
91
+ this._altInput = altInput;
92
+
93
+ box.append(title, urlLabel, urlInput, altLabel, altInput);
94
+
95
+ // Alignment
96
+ const alignLabel = createElement('label', { class: 'an-label' });
97
+ alignLabel.textContent = 'Alignment';
98
+ const alignRow = createElement('div', { class: 'an-align-row' });
99
+ const alignOptions = [
100
+ { value: '', label: 'None' },
101
+ { value: 'left', label: 'Left' },
102
+ { value: 'center', label: 'Center' },
103
+ { value: 'right', label: 'Right' },
104
+ ];
105
+ alignOptions.forEach(({ value, label }) => {
106
+ const radioId = `an-align-${value || 'none'}`;
107
+ const radio = createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value });
108
+ if (value === '') radio.checked = true;
109
+ const lbl = createElement('label', { for: radioId, class: 'an-align-label' });
110
+ lbl.textContent = label;
111
+ alignRow.append(radio, lbl);
112
+ });
113
+ this._alignRow = alignRow;
114
+ box.append(alignLabel, alignRow);
115
+ if (this.options.allowImageUpload !== false) {
116
+ const fileLabel = createElement('label', { class: 'an-label' });
117
+ fileLabel.textContent = 'Or upload a file';
118
+ const fileInput = createElement('input', {
119
+ type: 'file',
120
+ class: 'an-input',
121
+ accept: 'image/*',
122
+ });
123
+ this._fileInput = fileInput;
124
+ const d = on(fileInput, 'change', () => this._onFileChange());
125
+ this._disposers.push(d);
126
+ box.append(fileLabel, fileInput);
127
+ }
128
+
129
+ // Buttons
130
+ const btnRow = createElement('div', { class: 'an-dialog-actions' });
131
+ const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
132
+ insertBtn.textContent = 'Insert';
133
+ const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
134
+ cancelBtn.textContent = 'Cancel';
135
+ btnRow.appendChild(insertBtn);
136
+ btnRow.appendChild(cancelBtn);
137
+
138
+ box.append(btnRow);
139
+ overlay.appendChild(box);
140
+
141
+ const d1 = on(insertBtn, 'click', () => this._onInsert());
142
+ const d2 = on(cancelBtn, 'click', () => this._close());
143
+ const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
144
+ const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
145
+ const d5 = on(altInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
146
+ this._disposers.push(d1, d2, d3, d4, d5);
147
+
148
+ return overlay;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Actions
153
+ // ---------------------------------------------------------------------------
154
+
155
+ _onFileChange() {
156
+ const file = this._fileInput && this._fileInput.files && this._fileInput.files[0];
157
+ if (!file || !file.type.startsWith('image/')) return;
158
+
159
+ // Validate file size (max 5 MB by default)
160
+ const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
161
+ if (file.size > maxSize) {
162
+ const message = `Image file is too large. Maximum allowed size is ${this.options.maxImageSize || 5} MB.`;
163
+ console.warn('[AutumnNote] ImageDialog:', message);
164
+ this.context.triggerEvent('imageError', { file, message });
165
+ this._fileInput.value = '';
166
+ return;
167
+ }
168
+
169
+ const reader = new FileReader();
170
+ reader.onload = (e) => {
171
+ this._urlInput.value = e.target.result;
172
+ };
173
+ reader.readAsDataURL(file);
174
+ }
175
+
176
+ _onInsert() {
177
+ const src = this._urlInput.value.trim();
178
+ const alt = this._altInput.value.trim();
179
+ const alignRadio = this._alignRow && this._alignRow.querySelector('input[name="an-img-align"]:checked');
180
+ const align = alignRadio ? alignRadio.value : '';
181
+
182
+ if (!src) {
183
+ this._urlInput.focus();
184
+ return;
185
+ }
186
+
187
+ if (this._savedRange) {
188
+ this._savedRange.select();
189
+ }
190
+
191
+ this.context.invoke('editor.insertImage', src, alt, align);
192
+ this._close();
193
+ }
194
+
195
+ _open() {
196
+ if (this._dialog) {
197
+ this._dialog.style.display = 'flex';
198
+ this._removeTrap = trapFocus(this._dialog, () => this._close());
199
+ setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
200
+ }
201
+ }
202
+
203
+ _close() {
204
+ if (this._dialog) this._dialog.style.display = 'none';
205
+ if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
206
+ this._savedRange = null;
207
+ }
208
+ }
@@ -0,0 +1,216 @@
1
+ // ImageResizer.js - Interactive resize handles for selected images in the editor
2
+ import { on } from '../core/dom.js';
3
+
4
+ /**
5
+ * Eight handle positions. 'pos' is used as CSS class suffix and
6
+ * to determine which axis/direction is being dragged.
7
+ *
8
+ * Pos length 2 → corner handle → maintain aspect ratio.
9
+ * Pos length 1 → edge handle → single-axis resize.
10
+ */
11
+ const HANDLE_DEFS = [
12
+ { pos: 'nw', cursor: 'nw-resize' },
13
+ { pos: 'n', cursor: 'n-resize' },
14
+ { pos: 'ne', cursor: 'ne-resize' },
15
+ { pos: 'e', cursor: 'e-resize' },
16
+ { pos: 'se', cursor: 'se-resize' },
17
+ { pos: 's', cursor: 's-resize' },
18
+ { pos: 'sw', cursor: 'sw-resize' },
19
+ { pos: 'w', cursor: 'w-resize' },
20
+ ];
21
+
22
+ export class ImageResizer {
23
+ /** @param {import('../Context.js').Context} context */
24
+ constructor(context) {
25
+ this.context = context;
26
+ /** @type {HTMLImageElement|null} */
27
+ this._activeImg = null;
28
+ this._overlay = null;
29
+ this._disposers = [];
30
+ }
31
+
32
+ initialize() {
33
+ this._overlay = this._buildOverlay();
34
+ document.body.appendChild(this._overlay);
35
+
36
+ const editable = this.context.layoutInfo.editable;
37
+
38
+ this._disposers.push(
39
+ on(editable, 'click', (e) => this._onEditorClick(e)),
40
+ // Also select on right-click so the highlight shows before the context menu
41
+ on(editable, 'contextmenu', (e) => {
42
+ const img = e.target.closest('img');
43
+ if (img) this._select(img);
44
+ }),
45
+ on(document, 'click', (e) => this._onDocClick(e)),
46
+ on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
47
+ on(window, 'resize', () => this._updateOverlayPosition()),
48
+ on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
49
+ );
50
+
51
+ return this;
52
+ }
53
+
54
+ destroy() {
55
+ this._disposers.forEach((d) => d());
56
+ this._disposers = [];
57
+ if (this._dragDisposers) {
58
+ this._dragDisposers.forEach((d) => d());
59
+ this._dragDisposers = null;
60
+ }
61
+ this._deselect();
62
+ if (this._overlay && this._overlay.parentNode) {
63
+ this._overlay.parentNode.removeChild(this._overlay);
64
+ }
65
+ this._overlay = null;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Public API used by other modules
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /** @returns {HTMLImageElement|null} */
73
+ getActiveImage() {
74
+ return this._activeImg;
75
+ }
76
+
77
+ /** Re-sync overlay position (call after external size changes). */
78
+ updateOverlay() {
79
+ this._updateOverlayPosition();
80
+ }
81
+
82
+ /** Programmatically clear the current image selection. */
83
+ deselect() {
84
+ this._deselect();
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Internal
89
+ // ---------------------------------------------------------------------------
90
+
91
+ _buildOverlay() {
92
+ const overlay = document.createElement('div');
93
+ overlay.className = 'an-image-resizer';
94
+ overlay.style.display = 'none';
95
+
96
+ HANDLE_DEFS.forEach(({ pos }) => {
97
+ const h = document.createElement('div');
98
+ h.className = `an-resize-handle an-resize-${pos}`;
99
+ h.dataset.handle = pos;
100
+ // Attach handle listeners here so they're torn down in destroy() via _disposers
101
+ this._disposers.push(
102
+ on(h, 'mousedown', (e) => {
103
+ e.preventDefault();
104
+ e.stopPropagation();
105
+ this._startResize(e, pos);
106
+ }),
107
+ );
108
+ overlay.appendChild(h);
109
+ });
110
+
111
+ return overlay;
112
+ }
113
+
114
+ _onEditorClick(e) {
115
+ const img = e.target.closest('img');
116
+ if (img) {
117
+ // Prevent browser from deselecting the current text selection / opening native resize
118
+ e.preventDefault();
119
+ this._select(img);
120
+ }
121
+ }
122
+
123
+ _onDocClick(e) {
124
+ if (!this._activeImg) return;
125
+ if (e.target === this._activeImg) return;
126
+ if (this._overlay && this._overlay.contains(e.target)) return;
127
+ // Don't deselect while interacting with the context menu
128
+ if (e.target.closest('.an-contextmenu')) return;
129
+ this._deselect();
130
+ }
131
+
132
+ _select(img) {
133
+ if (this._activeImg && this._activeImg !== img) {
134
+ this._activeImg.classList.remove('an-image-selected');
135
+ }
136
+ this._activeImg = img;
137
+ img.classList.add('an-image-selected');
138
+ this._updateOverlayPosition();
139
+ this._overlay.style.display = 'block';
140
+ }
141
+
142
+ _deselect() {
143
+ if (this._activeImg) {
144
+ this._activeImg.classList.remove('an-image-selected');
145
+ this._activeImg = null;
146
+ }
147
+ if (this._overlay) this._overlay.style.display = 'none';
148
+ }
149
+
150
+ _updateOverlayPosition() {
151
+ if (!this._activeImg || !this._overlay) return;
152
+ const rect = this._activeImg.getBoundingClientRect();
153
+ this._overlay.style.left = `${rect.left}px`;
154
+ this._overlay.style.top = `${rect.top}px`;
155
+ this._overlay.style.width = `${rect.width}px`;
156
+ this._overlay.style.height = `${rect.height}px`;
157
+ }
158
+
159
+ _startResize(e, pos) {
160
+ const img = this._activeImg;
161
+ if (!img) return;
162
+
163
+ const startX = e.clientX;
164
+ const startY = e.clientY;
165
+ const startW = img.offsetWidth || img.naturalWidth || 100;
166
+ const startH = img.offsetHeight || img.naturalHeight || 100;
167
+ const aspectRatio = startW / (startH || 1);
168
+ const isCorner = pos.length === 2; // 'nw','ne','se','sw'
169
+
170
+ const editable = this.context.layoutInfo.editable;
171
+ const onMove = (me) => {
172
+ const dx = me.clientX - startX;
173
+ const dy = me.clientY - startY;
174
+ const maxW = editable.clientWidth || Infinity;
175
+ let newW = startW;
176
+ let newH = startH;
177
+
178
+ if (pos.includes('e')) newW = Math.max(20, startW + dx);
179
+ if (pos.includes('w')) newW = Math.max(20, startW - dx);
180
+ if (pos.includes('s')) newH = Math.max(20, startH + dy);
181
+ if (pos.includes('n')) newH = Math.max(20, startH - dy);
182
+
183
+ // Clamp to container width
184
+ newW = Math.min(newW, maxW);
185
+
186
+ if (isCorner) {
187
+ // Lock aspect ratio: use larger absolute delta to drive both dimensions
188
+ if (Math.abs(dx) >= Math.abs(dy)) {
189
+ newH = Math.max(20, Math.round(newW / aspectRatio));
190
+ } else {
191
+ newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
192
+ newH = Math.max(20, Math.round(newW / aspectRatio));
193
+ }
194
+ }
195
+
196
+ img.style.width = `${newW}px`;
197
+ img.style.height = `${newH}px`;
198
+ this._updateOverlayPosition();
199
+ };
200
+
201
+ const onUp = () => {
202
+ document.removeEventListener('mousemove', onMove);
203
+ document.removeEventListener('mouseup', onUp);
204
+ this._dragDisposers = null;
205
+ this.context.invoke('editor.afterCommand');
206
+ };
207
+
208
+ document.addEventListener('mousemove', onMove);
209
+ document.addEventListener('mouseup', onUp);
210
+ // Track these so destroy() can clean them up if called during an active drag
211
+ this._dragDisposers = [
212
+ () => document.removeEventListener('mousemove', onMove),
213
+ () => document.removeEventListener('mouseup', onUp),
214
+ ];
215
+ }
216
+ }