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,286 @@
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
+
6
+ const ICONS = {
7
+ 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>`,
8
+ 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>`,
9
+ 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>`,
10
+ 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>`,
11
+ 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>`,
12
+ 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>`,
13
+ 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>`,
14
+ };
15
+
16
+ const SHOW_DELAY = 100;
17
+ const HIDE_DELAY = 180;
18
+
19
+ export class ImageTooltip {
20
+ /** @param {import('../Context.js').Context} context */
21
+ constructor(context) {
22
+ this.context = context;
23
+ this._el = null;
24
+ this._activeImg = null;
25
+ this._showTimer = null;
26
+ this._hideTimer = null;
27
+ this._disposers = [];
28
+ }
29
+
30
+ initialize() {
31
+ this._el = this._buildTooltip();
32
+ document.body.appendChild(this._el);
33
+
34
+ const editable = this.context.layoutInfo.editable;
35
+
36
+ this._disposers.push(
37
+ on(editable, 'mouseover', (e) => {
38
+ const img = e.target.closest('img');
39
+ if (img && editable.contains(img)) {
40
+ this._scheduleShow(img);
41
+ }
42
+ }),
43
+ on(editable, 'mouseout', (e) => {
44
+ const to = e.relatedTarget;
45
+ if (!to || (!editable.contains(to) && !this._el.contains(to))) {
46
+ this._scheduleHide();
47
+ }
48
+ }),
49
+ // Hide when image is deselected by clicking elsewhere
50
+ on(document, 'click', (e) => {
51
+ if (this._activeImg && !this._activeImg.contains(e.target) && !this._el.contains(e.target)) {
52
+ this._hide();
53
+ }
54
+ }),
55
+ );
56
+
57
+ return this;
58
+ }
59
+
60
+ destroy() {
61
+ this._clearTimers();
62
+ this._disposers.forEach((d) => d());
63
+ this._disposers = [];
64
+ if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
65
+ this._el = null;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Build
70
+ // ---------------------------------------------------------------------------
71
+
72
+ _buildTooltip() {
73
+ const el = createElement('div', {
74
+ class: 'an-link-tooltip an-image-tooltip',
75
+ role: 'toolbar',
76
+ 'aria-label': 'Image actions',
77
+ });
78
+ el.style.display = 'none';
79
+
80
+ // Label showing "Image"
81
+ this._label = createElement('span', { class: 'an-link-tooltip-url' });
82
+ this._label.textContent = 'Image';
83
+ el.appendChild(this._label);
84
+
85
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
86
+
87
+ this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, 'Float Left', () => this._setFloat('left'));
88
+ this._floatNoneBtn = this._makeBtn(ICONS.floatNone, 'No Float', () => this._setFloat(''));
89
+ this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, 'Align Center', () => this._setCenter());
90
+ this._floatRightBtn = this._makeBtn(ICONS.floatRight, 'Float Right', () => this._setFloat('right'));
91
+
92
+ el.appendChild(this._floatLeftBtn);
93
+ el.appendChild(this._floatNoneBtn);
94
+ el.appendChild(this._alignCenterBtn);
95
+ el.appendChild(this._floatRightBtn);
96
+
97
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
98
+
99
+ this._originalBtn = this._makeBtn(ICONS.originalSize, 'Original Size', () => this._resetSize());
100
+
101
+ el.appendChild(this._originalBtn);
102
+
103
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
104
+
105
+ this._captionBtn = this._makeBtn(ICONS.caption, 'Add / Edit Caption', () => this._toggleCaption());
106
+ el.appendChild(this._captionBtn);
107
+
108
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
109
+
110
+ this._deleteBtn = this._makeBtn(ICONS.deleteImg, 'Delete Image', () => this._delete(), true);
111
+
112
+ el.appendChild(this._deleteBtn);
113
+
114
+ // Keep tooltip alive while hovering it
115
+ this._disposers.push(
116
+ on(el, 'mouseenter', () => this._clearTimers()),
117
+ on(el, 'mouseleave', () => this._scheduleHide()),
118
+ );
119
+
120
+ return el;
121
+ }
122
+
123
+ /**
124
+ * @param {string} icon
125
+ * @param {string} title
126
+ * @param {Function} handler
127
+ * @param {boolean} [isDanger]
128
+ */
129
+ _makeBtn(icon, title, handler, isDanger = false) {
130
+ const btn = createElement('button', {
131
+ type: 'button',
132
+ class: isDanger ? 'an-link-tooltip-btn an-link-tooltip-btn--danger' : 'an-link-tooltip-btn',
133
+ title,
134
+ });
135
+ btn.innerHTML = icon;
136
+ this._disposers.push(on(btn, 'click', (e) => {
137
+ e.preventDefault();
138
+ e.stopPropagation();
139
+ handler();
140
+ }));
141
+ return btn;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Show / Hide
146
+ // ---------------------------------------------------------------------------
147
+
148
+ _scheduleShow(img) {
149
+ if (this._activeImg === img && this._el.style.display !== 'none') return;
150
+ clearTimeout(this._hideTimer);
151
+ this._hideTimer = null;
152
+ clearTimeout(this._showTimer);
153
+ this._showTimer = setTimeout(() => {
154
+ this._activeImg = img;
155
+ this._show(img);
156
+ }, SHOW_DELAY);
157
+ }
158
+
159
+ _scheduleHide() {
160
+ clearTimeout(this._showTimer);
161
+ this._showTimer = null;
162
+ if (this._hideTimer) return;
163
+ this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
164
+ }
165
+
166
+ _show(img) {
167
+ this._el.style.display = 'flex';
168
+ this._positionNear(img);
169
+ }
170
+
171
+ _hide() {
172
+ this._el.style.display = 'none';
173
+ this._activeImg = null;
174
+ this._clearTimers();
175
+ }
176
+
177
+ _clearTimers() {
178
+ clearTimeout(this._showTimer);
179
+ clearTimeout(this._hideTimer);
180
+ this._showTimer = null;
181
+ this._hideTimer = null;
182
+ }
183
+
184
+ _positionNear(img) {
185
+ const rect = img.getBoundingClientRect();
186
+ const tipW = this._el.offsetWidth || 220;
187
+ const tipH = this._el.offsetHeight || 32;
188
+ const margin = 6;
189
+
190
+ let top = rect.bottom + margin;
191
+ let left = rect.left + (rect.width - tipW) / 2;
192
+
193
+ if (top + tipH > window.innerHeight - margin) top = rect.top - tipH - margin;
194
+ if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
195
+ if (left < margin) left = margin;
196
+
197
+ this._el.style.top = `${top}px`;
198
+ this._el.style.left = `${left}px`;
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Actions
203
+ // ---------------------------------------------------------------------------
204
+
205
+ _setFloat(value) {
206
+ const img = this._activeImg;
207
+ if (!img) return;
208
+ img.style.float = value;
209
+ img.style.display = '';
210
+ img.style.marginLeft = value === 'right' ? '12px' : '';
211
+ img.style.marginRight = value === 'left' ? '12px' : '';
212
+ this.context.invoke('editor.afterCommand');
213
+ this.context.invoke('imageResizer.updateOverlay');
214
+ this._positionNear(img);
215
+ }
216
+
217
+ _setCenter() {
218
+ const img = this._activeImg;
219
+ if (!img) return;
220
+ img.style.float = '';
221
+ img.style.display = 'block';
222
+ img.style.marginLeft = 'auto';
223
+ img.style.marginRight = 'auto';
224
+ this.context.invoke('editor.afterCommand');
225
+ this.context.invoke('imageResizer.updateOverlay');
226
+ this._positionNear(img);
227
+ }
228
+
229
+ _resetSize() {
230
+ const img = this._activeImg;
231
+ if (!img) return;
232
+ img.style.width = '';
233
+ img.style.height = '';
234
+ this.context.invoke('editor.afterCommand');
235
+ this.context.invoke('imageResizer.updateOverlay');
236
+ this._positionNear(img);
237
+ }
238
+
239
+ _delete() {
240
+ const img = this._activeImg;
241
+ if (!img) return;
242
+ this._hide();
243
+ this.context.invoke('imageResizer.deselect');
244
+ if (img.parentNode) img.parentNode.removeChild(img);
245
+ this.context.invoke('editor.afterCommand');
246
+ }
247
+
248
+ _toggleCaption() {
249
+ const img = this._activeImg;
250
+ if (!img) return;
251
+
252
+ // If already inside a figure, move focus to the figcaption
253
+ const existing = img.closest('figure.an-figure');
254
+ if (existing) {
255
+ const cap = existing.querySelector('figcaption.an-figcaption');
256
+ if (cap) {
257
+ this._hide();
258
+ const range = document.createRange();
259
+ range.selectNodeContents(cap);
260
+ const sel = window.getSelection();
261
+ if (sel) { sel.removeAllRanges(); sel.addRange(range); }
262
+ }
263
+ return;
264
+ }
265
+
266
+ // Wrap image in <figure><figcaption>
267
+ const figure = document.createElement('figure');
268
+ figure.className = 'an-figure';
269
+ const figcaption = document.createElement('figcaption');
270
+ figcaption.className = 'an-figcaption';
271
+ figcaption.textContent = 'Caption';
272
+
273
+ img.parentNode.insertBefore(figure, img);
274
+ figure.appendChild(img);
275
+ figure.appendChild(figcaption);
276
+
277
+ // Select caption text immediately
278
+ const range = document.createRange();
279
+ range.selectNodeContents(figcaption);
280
+ const sel = window.getSelection();
281
+ if (sel) { sel.removeAllRanges(); sel.addRange(range); }
282
+
283
+ this.context.invoke('editor.afterCommand');
284
+ this._hide();
285
+ }
286
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * LinkDialog.js - Dialog for inserting / editing hyperlinks
3
+ * Inspired by Summernote's LinkDialog — 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 LinkDialog {
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
+ /**
46
+ * Opens the link dialog.
47
+ * Pre-fills with the currently selected link if present.
48
+ */
49
+ show() {
50
+ withSavedRange((range) => {
51
+ this._savedRange = range;
52
+ });
53
+ this._prefill();
54
+ this._open();
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Build dialog
59
+ // ---------------------------------------------------------------------------
60
+
61
+ _buildDialog() {
62
+ const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Insert link' });
63
+ const box = createElement('div', { class: 'an-dialog-box' });
64
+
65
+ const title = createElement('h3', { class: 'an-dialog-title' });
66
+ title.textContent = 'Insert Link';
67
+
68
+ // URL field
69
+ const urlLabel = createElement('label', { class: 'an-label' });
70
+ urlLabel.textContent = 'URL';
71
+ const urlInput = createElement('input', {
72
+ type: 'url',
73
+ class: 'an-input',
74
+ placeholder: 'https://',
75
+ id: 'an-link-url',
76
+ name: 'url',
77
+ autocomplete: 'off',
78
+ });
79
+ this._urlInput = urlInput;
80
+
81
+ // Text field
82
+ const textLabel = createElement('label', { class: 'an-label' });
83
+ textLabel.textContent = 'Display Text';
84
+ const textInput = createElement('input', {
85
+ type: 'text',
86
+ class: 'an-input',
87
+ placeholder: 'Link text',
88
+ id: 'an-link-text',
89
+ name: 'linkText',
90
+ autocomplete: 'off',
91
+ });
92
+ this._textInput = textInput;
93
+
94
+ // Open in new tab
95
+ const tabLabel = createElement('label', { class: 'an-label an-label-inline' });
96
+ const tabCheckbox = createElement('input', {
97
+ type: 'checkbox',
98
+ id: 'an-link-newtab',
99
+ name: 'openInNewTab',
100
+ });
101
+ this._tabCheckbox = tabCheckbox;
102
+ tabLabel.appendChild(tabCheckbox);
103
+ tabLabel.appendChild(document.createTextNode(' Open in new tab'));
104
+
105
+ // Buttons
106
+ const btnRow = createElement('div', { class: 'an-dialog-actions' });
107
+ const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
108
+ insertBtn.textContent = 'Insert';
109
+ const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
110
+ cancelBtn.textContent = 'Cancel';
111
+ btnRow.appendChild(insertBtn);
112
+ btnRow.appendChild(cancelBtn);
113
+
114
+ box.append(title, urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
115
+ overlay.appendChild(box);
116
+
117
+ // Events
118
+ const d1 = on(insertBtn, 'click', () => this._onInsert());
119
+ const d2 = on(cancelBtn, 'click', () => this._close());
120
+ const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
121
+ const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
122
+ const d5 = on(textInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
123
+ this._disposers.push(d1, d2, d3, d4, d5);
124
+
125
+ return overlay;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Actions
130
+ // ---------------------------------------------------------------------------
131
+
132
+ _prefill() {
133
+ // Try to find currently selected anchor
134
+ const sel = window.getSelection();
135
+ let anchor = null;
136
+ if (sel && sel.rangeCount > 0) {
137
+ let node = sel.getRangeAt(0).startContainer;
138
+ while (node) {
139
+ if (node.nodeName === 'A') { anchor = node; break; }
140
+ node = node.parentNode;
141
+ }
142
+ }
143
+ if (anchor) {
144
+ this._urlInput.value = anchor.getAttribute('href') || '';
145
+ this._textInput.value = anchor.textContent || '';
146
+ this._tabCheckbox.checked = anchor.getAttribute('target') === '_blank';
147
+ } else {
148
+ this._urlInput.value = '';
149
+ this._textInput.value = sel ? sel.toString() : '';
150
+ this._tabCheckbox.checked = false;
151
+ }
152
+ }
153
+
154
+ _onInsert() {
155
+ let url = this._urlInput.value.trim();
156
+ const text = this._textInput.value.trim();
157
+ const newTab = this._tabCheckbox.checked;
158
+
159
+ if (!url) {
160
+ this._urlInput.focus();
161
+ return;
162
+ }
163
+
164
+ // Auto-prefix with https:// if no protocol is present
165
+ if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
166
+ url = 'https://' + url;
167
+ }
168
+
169
+ // Block unsafe protocols
170
+ try {
171
+ const parsed = new URL(url);
172
+ if (/^javascript:/i.test(parsed.protocol) || /^vbscript:/i.test(parsed.protocol) || /^data:/i.test(parsed.protocol)) {
173
+ this._urlInput.focus();
174
+ return;
175
+ }
176
+ } catch {
177
+ // URL constructor failed — not a valid URL
178
+ this._urlInput.focus();
179
+ return;
180
+ }
181
+
182
+ // Restore selection then insert
183
+ if (this._savedRange) {
184
+ this._savedRange.select();
185
+ }
186
+
187
+ this.context.invoke('editor.insertLink', url, text, newTab);
188
+ this._close();
189
+ }
190
+
191
+ _open() {
192
+ if (this._dialog) {
193
+ this._dialog.style.display = 'flex';
194
+ this._removeTrap = trapFocus(this._dialog, () => this._close());
195
+ setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
196
+ }
197
+ }
198
+
199
+ _close() {
200
+ if (this._dialog) this._dialog.style.display = 'none';
201
+ if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
202
+ this._savedRange = null;
203
+ }
204
+ }