autumnnote 1.16.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +31 -2
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +1521 -6319
  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/emoji-data-BDQX5kh-.js +2331 -0
  10. package/dist/emoji-data-BDQX5kh-.js.map +1 -0
  11. package/package.json +6 -2
  12. package/src/js/Context.js +68 -8
  13. package/src/js/core/env.js +23 -10
  14. package/src/js/core/sanitise.js +81 -8
  15. package/src/js/i18n/all.js +27 -0
  16. package/src/js/i18n/index.js +45 -19
  17. package/src/js/index.js +12 -2
  18. package/src/js/index.umd.js +5 -0
  19. package/src/js/module/BaseMediaTooltip.js +142 -0
  20. package/src/js/module/BaseResizer.js +322 -0
  21. package/src/js/module/EmojiDialog.js +41 -494
  22. package/src/js/module/ImageResizer.js +23 -241
  23. package/src/js/module/ImageTooltip.js +16 -111
  24. package/src/js/module/TableTooltip.js +91 -220
  25. package/src/js/module/Toolbar.js +107 -2
  26. package/src/js/module/VideoResizer.js +38 -239
  27. package/src/js/module/VideoTooltip.js +27 -114
  28. package/src/js/module/emoji-data.js +496 -0
  29. package/src/js/module/table-grid.js +102 -0
  30. package/src/js/module/table-icons.js +33 -0
  31. package/src/styles/autumnnote.scss +11 -0
  32. package/types/i18n/all.d.ts +14 -0
  33. package/types/i18n/de.d.ts +4 -0
  34. package/types/i18n/en.d.ts +4 -0
  35. package/types/i18n/es.d.ts +4 -0
  36. package/types/i18n/fr.d.ts +4 -0
  37. package/types/i18n/ja.d.ts +4 -0
  38. package/types/i18n/ko.d.ts +4 -0
  39. package/types/i18n/vi.d.ts +4 -0
  40. package/types/i18n/zh.d.ts +4 -0
  41. package/types/index.d.ts +30 -4
@@ -0,0 +1,142 @@
1
+ /**
2
+ * BaseMediaTooltip.js — Shared hover-tooltip behaviour for media embeds.
3
+ *
4
+ * ImageTooltip and VideoTooltip duplicated their button factory, show/hide
5
+ * timers and viewport-aware positioning (~22% of both files). Those pieces live
6
+ * here; each subclass keeps only what genuinely differs — which element counts
7
+ * as the target, which buttons the bar contains, and the actions behind them.
8
+ *
9
+ * Mirrors the existing BaseDialog / BaseResizer pattern.
10
+ */
11
+
12
+ import { createElement, on } from '../core/dom.js';
13
+
14
+ /** Delay before a hovered target shows its tooltip. */
15
+ export const SHOW_DELAY = 100;
16
+ /** Grace period before a tooltip hides after the pointer leaves. */
17
+ export const HIDE_DELAY = 180;
18
+
19
+ export class BaseMediaTooltip {
20
+ /** @param {import('../Context.js').Context} context */
21
+ constructor(context) {
22
+ this.context = context;
23
+ this.options = context.options;
24
+ /** @type {HTMLElement|null} The tooltip bar */
25
+ this._el = null;
26
+ /** @type {HTMLElement|null} Currently hovered target */
27
+ this._active = null;
28
+ this._showTimer = null;
29
+ this._hideTimer = null;
30
+ this._disposers = [];
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Subclass hooks
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Whether `_scheduleHide` is allowed to start the hide countdown. Subclasses
39
+ * override to pin the tooltip open (e.g. while a video preview is playing).
40
+ * @returns {boolean}
41
+ */
42
+ _canHide() { return true; }
43
+
44
+ /** Runs just before the tooltip is hidden, for subclass-specific teardown. */
45
+ _beforeHide() {}
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Buttons
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Builds a tooltip button and registers its click disposer.
53
+ * @param {string} icon - inline SVG markup
54
+ * @param {string} title
55
+ * @param {() => void} handler
56
+ * @param {boolean} [isDanger]
57
+ * @returns {HTMLElement}
58
+ */
59
+ _makeBtn(icon, title, handler, isDanger = false) {
60
+ const btn = createElement('button', {
61
+ type: 'button',
62
+ class: isDanger ? 'an-link-tooltip-btn an-link-tooltip-btn--danger' : 'an-link-tooltip-btn',
63
+ title,
64
+ });
65
+ btn.innerHTML = icon;
66
+ this._disposers.push(on(btn, 'click', (e) => {
67
+ e.preventDefault();
68
+ e.stopPropagation();
69
+ handler();
70
+ }));
71
+ return btn;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Show / Hide
76
+ // ---------------------------------------------------------------------------
77
+
78
+ _scheduleShow(target) {
79
+ if (this._active === target && this._el.style.display !== 'none') return;
80
+ clearTimeout(this._hideTimer);
81
+ this._hideTimer = null;
82
+ clearTimeout(this._showTimer);
83
+ this._showTimer = setTimeout(() => {
84
+ this._active = target;
85
+ this._show(target);
86
+ }, SHOW_DELAY);
87
+ }
88
+
89
+ _scheduleHide() {
90
+ if (!this._canHide()) return;
91
+ clearTimeout(this._showTimer);
92
+ this._showTimer = null;
93
+ // Always reset the hide timer so rapid mouseout→mouseover sequences
94
+ // don't leave a stale timer that hides the tooltip prematurely.
95
+ clearTimeout(this._hideTimer);
96
+ this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
97
+ }
98
+
99
+ _show(_target) {
100
+ this._el.style.display = 'flex';
101
+ // Defer positioning: offsetWidth on a newly-visible element forces layout
102
+ requestAnimationFrame(() => {
103
+ if (this._active) this._positionNear(this._active);
104
+ });
105
+ }
106
+
107
+ _hide() {
108
+ this._beforeHide();
109
+ this._el.style.display = 'none';
110
+ this._active = null;
111
+ this._clearTimers();
112
+ }
113
+
114
+ _clearTimers() {
115
+ clearTimeout(this._showTimer);
116
+ clearTimeout(this._hideTimer);
117
+ this._showTimer = null;
118
+ this._hideTimer = null;
119
+ }
120
+
121
+ /**
122
+ * Places the tooltip under the target, flipping above and clamping
123
+ * horizontally when it would leave the viewport.
124
+ * @param {HTMLElement} target
125
+ */
126
+ _positionNear(target) {
127
+ const rect = target.getBoundingClientRect();
128
+ const tipW = this._el.offsetWidth || 220;
129
+ const tipH = this._el.offsetHeight || 32;
130
+ const margin = 6;
131
+
132
+ let top = rect.bottom + margin;
133
+ let left = rect.left + (rect.width - tipW) / 2;
134
+
135
+ if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
136
+ if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
137
+ if (left < margin) left = margin;
138
+
139
+ this._el.style.top = `${top}px`;
140
+ this._el.style.left = `${left}px`;
141
+ }
142
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * BaseResizer.js — Shared drag-to-resize overlay for selectable embeds.
3
+ *
4
+ * ImageResizer and VideoResizer were ~28% duplicate: identical overlay
5
+ * construction, selection tracking, scroll/resize repositioning and drag maths,
6
+ * differing only in what counts as a target and how the new size is applied.
7
+ * Subclasses now supply just those differences via the hooks below.
8
+ *
9
+ * Mirrors the existing BaseDialog pattern used by the dialog modules.
10
+ */
11
+
12
+ import { on } from '../core/dom.js';
13
+
14
+ /**
15
+ * Eight handle positions. 'pos' is used as CSS class suffix and to determine
16
+ * which axis/direction is being dragged.
17
+ *
18
+ * Pos length 2 → corner handle → maintain aspect ratio.
19
+ * Pos length 1 → edge handle → single-axis resize.
20
+ */
21
+ const HANDLE_DEFS = [
22
+ { pos: 'nw', cursor: 'nw-resize' },
23
+ { pos: 'n', cursor: 'n-resize' },
24
+ { pos: 'ne', cursor: 'ne-resize' },
25
+ { pos: 'e', cursor: 'e-resize' },
26
+ { pos: 'se', cursor: 'se-resize' },
27
+ { pos: 's', cursor: 's-resize' },
28
+ { pos: 'sw', cursor: 'sw-resize' },
29
+ { pos: 'w', cursor: 'w-resize' },
30
+ ];
31
+
32
+ export class BaseResizer {
33
+ /** @param {import('../Context.js').Context} context */
34
+ constructor(context) {
35
+ this.context = context;
36
+ /** @type {HTMLElement|null} Currently selected element */
37
+ this._active = null;
38
+ /** @type {HTMLElement|null} */
39
+ this._overlay = null;
40
+ this._disposers = [];
41
+ this._positionRaf = null;
42
+ /** @type {{l:number,t:number,w:number,h:number}|null} Last written overlay box */
43
+ this._lastOverlayPos = null;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Subclass hooks
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /** CSS class for the overlay element. @returns {string} */
51
+ get _overlayClass() { return 'an-resizer'; }
52
+
53
+ /** CSS class applied to the selected element. @returns {string} */
54
+ get _selectedClass() { return 'an-selected'; }
55
+
56
+ /**
57
+ * Resolves an event target to a resizable element.
58
+ * @param {EventTarget|null} _el
59
+ * @returns {HTMLElement|null}
60
+ */
61
+ _findTarget(_el) { return null; }
62
+
63
+ /**
64
+ * Natural starting dimensions of the target, used as the drag baseline.
65
+ * @param {HTMLElement} _target
66
+ * @returns {{ w: number, h: number }}
67
+ */
68
+ _getStartSize(_target) { return { w: 0, h: 0 }; }
69
+
70
+ /** Smallest allowed dimensions. @returns {{ minW: number, minH: number }} */
71
+ _getMinSize() { return { minW: 20, minH: 20 }; }
72
+
73
+ /**
74
+ * Writes the new size to the target (and any inner embed).
75
+ * @param {HTMLElement} _target
76
+ * @param {number} _w
77
+ * @param {number} _h
78
+ */
79
+ _applySize(_target, _w, _h) {}
80
+
81
+ /**
82
+ * Additional listeners a subclass needs; the returned disposers are torn down
83
+ * with the rest in destroy().
84
+ * @returns {Array<() => void>}
85
+ */
86
+ _extraListeners() { return []; }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Lifecycle
90
+ // ---------------------------------------------------------------------------
91
+
92
+ initialize() {
93
+ this._overlay = this._buildOverlay();
94
+ const container = this.context.layoutInfo.editable.closest('.an-container') || document.body;
95
+ container.appendChild(this._overlay);
96
+ this._container = container;
97
+
98
+ const editable = this.context.layoutInfo.editable;
99
+
100
+ // Debounce window resize — _updateOverlayPosition already has rAF gating but
101
+ // every call cancels + re-schedules it; a debounce reduces that churn.
102
+ let resizeDebounce = null;
103
+ const onWindowResize = () => {
104
+ clearTimeout(resizeDebounce);
105
+ resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
106
+ };
107
+
108
+ this._disposers.push(
109
+ on(editable, 'click', (e) => this._onEditorClick(e)),
110
+ // Also select on right-click so the highlight shows before the context menu
111
+ on(editable, 'contextmenu', (e) => {
112
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
113
+ const target = this._findTarget(e.target);
114
+ if (target) this._select(target);
115
+ }),
116
+ on(document, 'click', (e) => this._onDocClick(e)),
117
+ on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
118
+ on(globalThis, 'resize', onWindowResize, { passive: true }),
119
+ on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
120
+ ...this._extraListeners(),
121
+ );
122
+
123
+ return this;
124
+ }
125
+
126
+ destroy() {
127
+ this._disposers.forEach((d) => d());
128
+ this._disposers = [];
129
+ if (this._dragDisposers) {
130
+ this._dragDisposers.forEach((d) => d());
131
+ this._dragDisposers = null;
132
+ }
133
+ if (this._positionRaf) {
134
+ cancelAnimationFrame(this._positionRaf);
135
+ this._positionRaf = null;
136
+ }
137
+ this._deselect();
138
+ this._overlay?.remove();
139
+ this._overlay = null;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Public API used by other modules
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /** Re-sync overlay position (call after external size changes). */
147
+ updateOverlay() {
148
+ this._updateOverlayPosition();
149
+ }
150
+
151
+ /** Programmatically clear the current selection. */
152
+ deselect() {
153
+ this._deselect();
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Internal
158
+ // ---------------------------------------------------------------------------
159
+
160
+ _buildOverlay() {
161
+ const overlay = document.createElement('div');
162
+ overlay.className = this._overlayClass;
163
+ overlay.style.display = 'none';
164
+
165
+ HANDLE_DEFS.forEach(({ pos }) => {
166
+ const h = document.createElement('div');
167
+ h.className = `an-resize-handle an-resize-${pos}`;
168
+ h.dataset.handle = pos;
169
+ // Attach handle listeners here so they're torn down in destroy() via _disposers
170
+ this._disposers.push(
171
+ // Pointer events rather than mouse events, so the same code path serves
172
+ // mouse, touch and pen. The handles also set `touch-action: none` in
173
+ // CSS — without it a touch drag is claimed by the browser as a scroll
174
+ // gesture and the pointer stream is cancelled before it starts.
175
+ on(h, 'pointerdown', (e) => {
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ this._startResize(e, pos);
179
+ }),
180
+ );
181
+ overlay.appendChild(h);
182
+ });
183
+
184
+ return overlay;
185
+ }
186
+
187
+ _onEditorClick(e) {
188
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
189
+ const target = this._findTarget(e.target);
190
+ if (target) {
191
+ // Prevent the browser from dropping the text selection / showing its own
192
+ // native resize affordance.
193
+ e.preventDefault();
194
+ this._select(target);
195
+ }
196
+ }
197
+
198
+ _onDocClick(e) {
199
+ if (!this._active) return;
200
+ if (this._active.contains(e.target)) return;
201
+ if (this._overlay?.contains(e.target)) return;
202
+ // Don't deselect while interacting with the context menu
203
+ if (e.target.closest?.('.an-contextmenu')) return;
204
+ this._deselect();
205
+ }
206
+
207
+ _select(target) {
208
+ if (this._active && this._active !== target) {
209
+ this._active.classList.remove(this._selectedClass);
210
+ }
211
+ this._active = target;
212
+ this._lastOverlayPos = null; // invalidate position cache on new selection
213
+ target.classList.add(this._selectedClass);
214
+ this._updateOverlayPosition();
215
+ this._overlay.style.display = 'block';
216
+ }
217
+
218
+ _deselect() {
219
+ if (this._active) {
220
+ this._active.classList.remove(this._selectedClass);
221
+ this._active = null;
222
+ }
223
+ if (this._overlay) this._overlay.style.display = 'none';
224
+ }
225
+
226
+ _updateOverlayPosition() {
227
+ if (this._positionRaf) cancelAnimationFrame(this._positionRaf);
228
+ this._positionRaf = requestAnimationFrame(() => {
229
+ this._positionRaf = null;
230
+ this._updateOverlayPositionNow();
231
+ });
232
+ }
233
+
234
+ _updateOverlayPositionNow() {
235
+ if (!this._active || !this._overlay) return;
236
+ const offsetParent = this._overlay.offsetParent || this._container;
237
+ const containerRect = offsetParent.getBoundingClientRect();
238
+ const rect = this._active.getBoundingClientRect();
239
+
240
+ // Skip DOM writes when position hasn't changed (common during non-scroll rAF ticks)
241
+ const p = this._lastOverlayPos;
242
+ if (p?.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
243
+ this._lastOverlayPos = { l: rect.left, t: rect.top, w: rect.width, h: rect.height };
244
+
245
+ const left = rect.left - containerRect.left + offsetParent.scrollLeft;
246
+ const top = rect.top - containerRect.top + offsetParent.scrollTop;
247
+ this._overlay.style.left = `${left}px`;
248
+ this._overlay.style.top = `${top}px`;
249
+ this._overlay.style.width = `${rect.width}px`;
250
+ this._overlay.style.height = `${rect.height}px`;
251
+ }
252
+
253
+ _startResize(e, pos) {
254
+ const target = this._active;
255
+ if (!target) return;
256
+
257
+ const startX = e.clientX;
258
+ const startY = e.clientY;
259
+ const { w: startW, h: startH } = this._getStartSize(target);
260
+ const { minW, minH } = this._getMinSize();
261
+ const aspectRatio = startW / (startH || 1);
262
+ const isCorner = pos.length === 2; // 'nw','ne','se','sw'
263
+
264
+ const editable = this.context.layoutInfo.editable;
265
+ let raf = null; // rAF handle — ensures at most one write per paint frame
266
+
267
+ const onMove = (me) => {
268
+ if (raf !== null) return; // frame already pending, discard this event
269
+ const clientX = me.clientX;
270
+ const clientY = me.clientY;
271
+ raf = requestAnimationFrame(() => {
272
+ raf = null;
273
+ const dx = clientX - startX;
274
+ const dy = clientY - startY;
275
+ const maxW = editable.clientWidth || Infinity;
276
+ let newW = startW;
277
+ let newH = startH;
278
+
279
+ if (pos.includes('e')) newW = Math.max(minW, startW + dx);
280
+ if (pos.includes('w')) newW = Math.max(minW, startW - dx);
281
+ if (pos.includes('s')) newH = Math.max(minH, startH + dy);
282
+ if (pos.includes('n')) newH = Math.max(minH, startH - dy);
283
+
284
+ newW = Math.min(newW, maxW);
285
+
286
+ if (isCorner) {
287
+ if (Math.abs(dx) >= Math.abs(dy)) {
288
+ newH = Math.max(minH, Math.round(newW / aspectRatio));
289
+ } else {
290
+ newW = Math.min(Math.max(minW, Math.round(newH * aspectRatio)), maxW);
291
+ newH = Math.max(minH, Math.round(newW / aspectRatio));
292
+ }
293
+ }
294
+
295
+ this._applySize(target, newW, newH);
296
+ this._updateOverlayPosition();
297
+ });
298
+ };
299
+
300
+ const stop = () => {
301
+ document.removeEventListener('pointermove', onMove);
302
+ document.removeEventListener('pointerup', onUp);
303
+ document.removeEventListener('pointercancel', onUp);
304
+ };
305
+
306
+ const onUp = () => {
307
+ if (raf !== null) { cancelAnimationFrame(raf); raf = null; }
308
+ stop();
309
+ this._dragDisposers = null;
310
+ this.context.invoke('editor.afterCommand');
311
+ };
312
+
313
+ document.addEventListener('pointermove', onMove);
314
+ document.addEventListener('pointerup', onUp);
315
+ // A touch drag interrupted by the system (incoming call, gesture takeover)
316
+ // ends with pointercancel and no pointerup, which would otherwise leave the
317
+ // move listener attached for the rest of the session.
318
+ document.addEventListener('pointercancel', onUp);
319
+ // Track these so destroy() can clean them up if called during an active drag
320
+ this._dragDisposers = [stop];
321
+ }
322
+ }