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,586 +0,0 @@
1
- // ImageCropOverlay.js - Inline crop overlay for images inside the editor
2
- //
3
- // Architecture:
4
- // - A dark scrim fills the entire viewport (fixed, z=10100) with a transparent
5
- // cutout showing the current crop region (clip-path technique).
6
- // - A "crop box" div overlaps the cutout; four corner + four edge handles let
7
- // the user drag to resize it.
8
- // - On Confirm: draws the crop region onto an off-screen canvas and replaces
9
- // img.src with a data-URL. Cross-origin images are caught and a warning is
10
- // shown instead of crashing.
11
- // - All pointer listeners are document-level and are torn down after each
12
- // interaction to avoid leaks.
13
-
14
- import { on } from '../core/dom.js';
15
-
16
- // Minimum crop box dimension in CSS pixels
17
- const MIN_SIZE = 20;
18
-
19
- // Accent colour for handles (synced with $an-primary)
20
- const ACCENT = '#3b82f6';
21
-
22
- // ---------------------------------------------------------------------------
23
- // Helpers
24
- // ---------------------------------------------------------------------------
25
-
26
- /** Clamp `value` between `lo` and `hi`. */
27
- const clamp = (value, lo, hi) => Math.min(Math.max(value, lo), hi);
28
-
29
- /**
30
- * Attempt to draw `img` onto a canvas even if it was loaded without CORS.
31
- * Returns a canvas element on success, or null if the image is cross-origin
32
- * tainted and cannot be read.
33
- *
34
- * Strategy:
35
- * 1. Try directly — works for same-origin and data/blob URLs.
36
- * 2. Re-load with crossOrigin="anonymous" and retry (needs server ACAO header).
37
- *
38
- * @param {HTMLImageElement} img
39
- * @param {DOMRect} naturalRect - crop region in *natural* image pixels
40
- * @param {number} renderW - desired output width
41
- * @param {number} renderH - desired output height
42
- * @returns {Promise<HTMLCanvasElement|null>}
43
- */
44
- function drawCropToCanvas(img, naturalRect, renderW, renderH) {
45
- return new Promise((resolve) => {
46
- const tryDraw = (source) => {
47
- const canvas = document.createElement('canvas');
48
- canvas.width = Math.round(renderW);
49
- canvas.height = Math.round(renderH);
50
- const ctx = canvas.getContext('2d');
51
- if (!ctx) { resolve(null); return; } // GPU memory limit — let _confirm() handle null
52
- try {
53
- ctx.drawImage(
54
- source,
55
- naturalRect.x, naturalRect.y, naturalRect.width, naturalRect.height,
56
- 0, 0, canvas.width, canvas.height,
57
- );
58
- // Access a pixel — this throws if canvas is tainted
59
- ctx.getImageData(0, 0, 1, 1);
60
- resolve(canvas);
61
- } catch (_) {
62
- void _; // canvas tainted — resolve with null
63
- resolve(null);
64
- }
65
- };
66
-
67
- // Fast path — data: / blob: / same-origin
68
- if (
69
- img.src.startsWith('data:') ||
70
- img.src.startsWith('blob:') ||
71
- img.src.startsWith(globalThis.location.origin)
72
- ) {
73
- tryDraw(img);
74
- return;
75
- }
76
-
77
- // Cross-origin: try a fresh image with crossOrigin="anonymous"
78
- const tmp = new Image();
79
- tmp.crossOrigin = 'anonymous';
80
- tmp.onload = () => tryDraw(tmp);
81
- tmp.onerror = () => resolve(null);
82
- // Append cache-buster to avoid serving a cached non-CORS response
83
- tmp.src = img.src + (img.src.includes('?') ? '&' : '?') + '_an=' + Date.now();
84
- });
85
- }
86
-
87
- // ---------------------------------------------------------------------------
88
- // ImageCropOverlay
89
- // ---------------------------------------------------------------------------
90
-
91
- export class ImageCropOverlay {
92
- /** @param {import('../Context.js').Context} context */
93
- constructor(context) {
94
- this.context = context;
95
- /** @type {HTMLImageElement|null} */
96
- this._img = null;
97
- /** Crop box position in viewport px: { x, y, w, h } */
98
- this._box = null;
99
- /** Natural-size bounding rect of the image in viewport px */
100
- this._imgRect = null;
101
-
102
- this._scrim = null;
103
- this._cropBox = null;
104
- this._handles = {};
105
- this._toolbar = null;
106
- this._infoEl = null;
107
-
108
- this._dragDisposers = null;
109
- this._disposers = [];
110
- }
111
-
112
- initialize() {
113
- return this;
114
- }
115
-
116
- destroy() {
117
- this._disposers.forEach((d) => d());
118
- this._disposers = [];
119
- this._close(false);
120
- }
121
-
122
- // ---------------------------------------------------------------------------
123
- // Public API
124
- // ---------------------------------------------------------------------------
125
-
126
- /**
127
- * Open the crop overlay for the given image.
128
- * @param {HTMLImageElement} img
129
- */
130
- open(img) {
131
- if (this._scrim) this._close(false); // guard: only one at a time
132
-
133
- this._img = img;
134
- const rect = img.getBoundingClientRect();
135
- this._imgRect = rect;
136
-
137
- // Default crop box = full image, inset 12 px on each side (feels visual)
138
- const inset = Math.min(12, rect.width * 0.1, rect.height * 0.1);
139
- this._box = {
140
- x: rect.left + inset,
141
- y: rect.top + inset,
142
- w: rect.width - inset * 2,
143
- h: rect.height - inset * 2,
144
- };
145
-
146
- this._buildDOM();
147
- this._updateDOM();
148
- this._bindEsc();
149
- }
150
-
151
- // ---------------------------------------------------------------------------
152
- // DOM construction
153
- // ---------------------------------------------------------------------------
154
-
155
- _buildDOM() {
156
- /* ---- scrim ---- */
157
- const scrim = document.createElement('div');
158
- scrim.className = 'an-crop-scrim';
159
- // Prevent scroll during crop
160
- scrim.style.cssText = `
161
- position:fixed; inset:0; z-index:10100;
162
- cursor:crosshair;
163
- background: rgba(0,0,0,0.55);
164
- `;
165
- // Click on scrim (outside crop box) → cancel
166
- this._disposers.push(
167
- on(scrim, 'mousedown', (e) => {
168
- if (e.target === scrim) this._close(false);
169
- }),
170
- );
171
-
172
- /* ---- crop box ---- */
173
- const cropBox = document.createElement('div');
174
- cropBox.className = 'an-crop-box';
175
- cropBox.style.cssText = `
176
- position:fixed; z-index:10101;
177
- box-sizing:border-box;
178
- border:2px solid ${ACCENT};
179
- cursor:move;
180
- outline: none;
181
- `;
182
-
183
- /* ---- rule-of-thirds grid lines ---- */
184
- const grid = document.createElement('div');
185
- grid.className = 'an-crop-grid';
186
- grid.style.cssText = `
187
- position:absolute; inset:0; pointer-events:none;
188
- opacity:0.35;
189
- `;
190
- // 2 vertical + 2 horizontal lines
191
- ['33.33%','66.66%'].forEach((pos) => {
192
- const vl = document.createElement('div');
193
- vl.style.cssText = `position:absolute;top:0;bottom:0;left:${pos};width:1px;background:#fff;`;
194
- const hl = document.createElement('div');
195
- hl.style.cssText = `position:absolute;left:0;right:0;top:${pos};height:1px;background:#fff;`;
196
- grid.appendChild(vl);
197
- grid.appendChild(hl);
198
- });
199
- cropBox.appendChild(grid);
200
-
201
- /* ---- resize handles ---- */
202
- const HANDLE_DEFS = [
203
- { id: 'nw', cur: 'nw-resize', top: '-5px', left: '-5px' },
204
- { id: 'n', cur: 'n-resize', top: '-5px', left: 'calc(50% - 5px)' },
205
- { id: 'ne', cur: 'ne-resize', top: '-5px', right: '-5px' },
206
- { id: 'e', cur: 'e-resize', top: 'calc(50% - 5px)', right: '-5px' },
207
- { id: 'se', cur: 'se-resize', bottom: '-5px', right: '-5px' },
208
- { id: 's', cur: 's-resize', bottom: '-5px', left: 'calc(50% - 5px)' },
209
- { id: 'sw', cur: 'sw-resize', bottom: '-5px', left: '-5px' },
210
- { id: 'w', cur: 'w-resize', top: 'calc(50% - 5px)', left: '-5px' },
211
- ];
212
-
213
- HANDLE_DEFS.forEach(({ id, cur, ...pos }) => {
214
- const h = document.createElement('div');
215
- h.className = `an-crop-handle an-crop-handle-${id}`;
216
- h.style.cssText = [
217
- 'position:absolute',
218
- 'width:10px', 'height:10px',
219
- `background:${ACCENT}`,
220
- 'border:2px solid #fff',
221
- 'border-radius:2px',
222
- 'box-sizing:border-box',
223
- `cursor:${cur}`,
224
- ...Object.entries(pos).map(([k, v]) => `${k}:${v}`),
225
- ].join(';');
226
- this._disposers.push(
227
- on(h, 'mousedown', (e) => {
228
- e.preventDefault();
229
- e.stopPropagation();
230
- this._startHandleDrag(e, id);
231
- }),
232
- on(h, 'touchstart', (e) => {
233
- e.preventDefault();
234
- e.stopPropagation();
235
- const te = /** @type {TouchEvent} */ (e); this._startHandleDrag({ clientX: te.touches[0].clientX, clientY: te.touches[0].clientY }, id);
236
- }, { passive: false }),
237
- );
238
- this._handles[id] = h;
239
- cropBox.appendChild(h);
240
- });
241
-
242
- /* ---- crop move drag ---- */
243
- this._disposers.push(
244
- on(cropBox, 'mousedown', (e) => {
245
- if (/** @type {Element} */ (e.target).classList.contains('an-crop-handle')) return;
246
- if (e.target !== cropBox && e.target !== grid) return;
247
- e.preventDefault();
248
- e.stopPropagation();
249
- this._startBoxMove(e);
250
- }),
251
- on(cropBox, 'touchstart', (e) => {
252
- if (/** @type {Element} */ (e.target).classList.contains('an-crop-handle')) return;
253
- if (e.target !== cropBox && e.target !== grid) return;
254
- e.preventDefault();
255
- e.stopPropagation();
256
- const te2 = /** @type {TouchEvent} */ (e); this._startBoxMove({ clientX: te2.touches[0].clientX, clientY: te2.touches[0].clientY });
257
- }, { passive: false }),
258
- );
259
-
260
- /* ---- info label ---- */
261
- const infoEl = document.createElement('div');
262
- infoEl.className = 'an-crop-info';
263
- infoEl.style.cssText = `
264
- position:absolute; bottom:-28px; left:0;
265
- font:bold 11px/20px system-ui,sans-serif;
266
- color:#fff; white-space:nowrap;
267
- pointer-events:none;
268
- text-shadow: 0 1px 3px rgba(0,0,0,.6);
269
- `;
270
- cropBox.appendChild(infoEl);
271
- this._infoEl = infoEl;
272
-
273
- /* ---- floating toolbar ---- */
274
- const toolbar = document.createElement('div');
275
- toolbar.className = 'an-crop-toolbar';
276
- toolbar.style.cssText = `
277
- position:fixed; z-index:10102;
278
- background:#1e1e2e; border-radius:6px;
279
- padding:5px 8px; display:flex; gap:6px;
280
- align-items:center;
281
- box-shadow:0 4px 16px rgba(0,0,0,.4);
282
- font:13px system-ui,sans-serif;
283
- `;
284
-
285
- const confirmBtn = this._makeToolbarBtn('✓ Crop', '#22c55e', () => this._confirm());
286
- const cancelBtn = this._makeToolbarBtn('✕ Cancel', '#94a3b8', () => this._close(false));
287
-
288
- // Aspect ratio lock checkbox
289
- const arLabel = document.createElement('label');
290
- arLabel.style.cssText = 'color:#94a3b8;font-size:11px;cursor:pointer;display:flex;align-items:center;gap:4px;';
291
- const arCheck = document.createElement('input');
292
- arCheck.type = 'checkbox';
293
- arCheck.style.accentColor = ACCENT;
294
- arLabel.appendChild(arCheck);
295
- arLabel.appendChild(document.createTextNode('Lock ratio'));
296
- this._arCheck = arCheck;
297
-
298
- toolbar.appendChild(confirmBtn);
299
- toolbar.appendChild(cancelBtn);
300
- toolbar.appendChild(arLabel);
301
- this._toolbar = toolbar;
302
-
303
- // Prevent toolbar clicks from closing the overlay
304
- this._disposers.push(
305
- on(toolbar, 'mousedown', (e) => e.stopPropagation()),
306
- );
307
-
308
- document.body.appendChild(scrim);
309
- document.body.appendChild(cropBox);
310
- document.body.appendChild(toolbar);
311
-
312
- this._scrim = scrim;
313
- this._cropBox = cropBox;
314
- }
315
-
316
- _makeToolbarBtn(text, color, handler) {
317
- const btn = document.createElement('button');
318
- btn.type = 'button';
319
- btn.textContent = text;
320
- btn.style.cssText = `
321
- background:none; border:1px solid ${color}; border-radius:4px;
322
- color:${color}; padding:3px 10px; cursor:pointer; font-size:12px;
323
- font-family:inherit;
324
- transition:background 0.12s;
325
- `;
326
- btn.addEventListener('mouseover', () => { btn.style.background = color + '22'; });
327
- btn.addEventListener('mouseout', () => { btn.style.background = 'none'; });
328
- this._disposers.push(on(btn, 'click', (e) => { e.stopPropagation(); handler(); }));
329
- return btn;
330
- }
331
-
332
- // ---------------------------------------------------------------------------
333
- // DOM sync
334
- // ---------------------------------------------------------------------------
335
-
336
- _updateDOM() {
337
- const { x, y, w, h } = this._box;
338
-
339
- this._cropBox.style.left = `${x}px`;
340
- this._cropBox.style.top = `${y}px`;
341
- this._cropBox.style.width = `${w}px`;
342
- this._cropBox.style.height = `${h}px`;
343
-
344
- // Scrim cutout via clip-path polygon (punches a transparent hole)
345
- const vw = globalThis.innerWidth;
346
- const vh = globalThis.innerHeight;
347
- // Outer rect → inner rectangle (counterclockwise) = hole
348
- this._scrim.style.clipPath = [
349
- `polygon(`,
350
- `0 0, ${vw}px 0, ${vw}px ${vh}px, 0 ${vh}px, 0 0,`,
351
- `${x}px ${y}px, ${x}px ${y + h}px, ${x + w}px ${y + h}px, ${x + w}px ${y}px, ${x}px ${y}px`,
352
- `)`,
353
- ].join('');
354
-
355
- // Info label: natural pixels
356
- if (this._img) {
357
- const nw = this._img.naturalWidth || 0;
358
- const nh = this._img.naturalHeight || 0;
359
- const dispW = this._imgRect.width || 1;
360
- const dispH = this._imgRect.height || 1;
361
- const scale = { x: nw / dispW, y: nh / dispH };
362
- const cx = clamp(x - this._imgRect.left, 0, dispW);
363
- const cy = clamp(y - this._imgRect.top, 0, dispH);
364
- const cw = clamp(w, 0, dispW - cx);
365
- const ch = clamp(h, 0, dispH - cy);
366
- this._infoEl.textContent = `${Math.round(cw * scale.x)} × ${Math.round(ch * scale.y)} px`;
367
- }
368
-
369
- // Position toolbar below crop box (or above if near bottom)
370
- const margin = 8;
371
- let tbTop = y + h + margin;
372
- if (tbTop + 40 > globalThis.innerHeight - margin) tbTop = y - 40 - margin;
373
- this._toolbar.style.left = `${x}px`;
374
- this._toolbar.style.top = `${tbTop}px`;
375
- }
376
-
377
- // ---------------------------------------------------------------------------
378
- // Drag: move box
379
- // ---------------------------------------------------------------------------
380
-
381
- _startBoxMove(e) {
382
- const startX = e.clientX - this._box.x;
383
- const startY = e.clientY - this._box.y;
384
- const r = this._imgRect;
385
-
386
- const onMove = (me) => {
387
- this._box.x = clamp(me.clientX - startX, r.left, r.right - this._box.w);
388
- this._box.y = clamp(me.clientY - startY, r.top, r.bottom - this._box.h);
389
- this._updateDOM();
390
- };
391
-
392
- this._attachDocDrag(onMove);
393
- }
394
-
395
- // ---------------------------------------------------------------------------
396
- // Drag: resize handle
397
- // ---------------------------------------------------------------------------
398
-
399
- _startHandleDrag(e, id) {
400
- const startX = e.clientX;
401
- const startY = e.clientY;
402
- const orig = { ...this._box };
403
- const r = this._imgRect;
404
- const lockAR = this._arCheck?.checked;
405
- const aspect = orig.w / (orig.h || 1);
406
-
407
- const onMove = (me) => {
408
- const dx = me.clientX - startX;
409
- const dy = me.clientY - startY;
410
- let { x, y, w, h } = orig;
411
-
412
- // Apply deltas per handle position
413
- if (id.includes('e')) w = Math.max(MIN_SIZE, orig.w + dx);
414
- if (id.includes('s')) h = Math.max(MIN_SIZE, orig.h + dy);
415
- if (id.includes('w')) { x = Math.min(orig.x + orig.w - MIN_SIZE, orig.x + dx); w = orig.x + orig.w - x; }
416
- if (id.includes('n')) { y = Math.min(orig.y + orig.h - MIN_SIZE, orig.y + dy); h = orig.y + orig.h - y; }
417
-
418
- // Aspect-ratio lock (corner handles)
419
- if (lockAR && id.length === 2) {
420
- if (Math.abs(dx) >= Math.abs(dy)) {
421
- h = w / aspect;
422
- if (id.includes('n')) y = orig.y + orig.h - h;
423
- } else {
424
- w = h * aspect;
425
- if (id.includes('w')) x = orig.x + orig.w - w;
426
- }
427
- }
428
-
429
- // Clamp to image bounds
430
- x = clamp(x, r.left, r.right - MIN_SIZE);
431
- y = clamp(y, r.top, r.bottom - MIN_SIZE);
432
- w = clamp(w, MIN_SIZE, r.right - x);
433
- h = clamp(h, MIN_SIZE, r.bottom - y);
434
-
435
- this._box = { x, y, w, h };
436
- this._updateDOM();
437
- };
438
-
439
- this._attachDocDrag(onMove);
440
- }
441
-
442
- /**
443
- * Attach mousemove + mouseup to document for the duration of a drag.
444
- * @param {(e: MouseEvent) => void} onMove
445
- */
446
- _attachDocDrag(onMove) {
447
- this._dragDisposers?.();
448
- const onTouchMove = (e) => {
449
- e.preventDefault();
450
- const te3 = /** @type {TouchEvent} */ (e); onMove(/** @type {MouseEvent} */ (/** @type {unknown} */ ({ clientX: te3.touches[0].clientX, clientY: te3.touches[0].clientY })));
451
- };
452
- const cleanup = () => {
453
- document.removeEventListener('mousemove', onMove);
454
- document.removeEventListener('mouseup', cleanup);
455
- document.removeEventListener('touchmove', onTouchMove);
456
- document.removeEventListener('touchend', cleanup);
457
- document.body.style.userSelect = '';
458
- document.body.style.cursor = '';
459
- this._dragDisposers = null;
460
- };
461
- this._dragDisposers = cleanup;
462
- document.body.style.userSelect = 'none';
463
- document.addEventListener('mousemove', onMove);
464
- document.addEventListener('mouseup', cleanup, { once: true });
465
- document.addEventListener('touchmove', onTouchMove, { passive: false });
466
- document.addEventListener('touchend', cleanup, { once: true });
467
- }
468
-
469
- // ---------------------------------------------------------------------------
470
- // Keyboard
471
- // ---------------------------------------------------------------------------
472
-
473
- _bindEsc() {
474
- const handler = (e) => {
475
- if (e.key === 'Escape') { e.preventDefault(); this._close(false); }
476
- if (e.key === 'Enter') { e.preventDefault(); this._confirm(); }
477
- };
478
- document.addEventListener('keydown', handler);
479
- this._disposers.push(() => document.removeEventListener('keydown', handler));
480
- }
481
-
482
- // ---------------------------------------------------------------------------
483
- // Confirm / Close
484
- // ---------------------------------------------------------------------------
485
-
486
- async _confirm() {
487
- const img = this._img;
488
- if (!img) { this._close(false); return; }
489
-
490
- const r = this._imgRect;
491
- const { x, y, w, h } = this._box;
492
-
493
- // Convert display-px crop region → natural-px
494
- const scaleX = (img.naturalWidth || img.width || 1) / (r.width || 1);
495
- const scaleY = (img.naturalHeight || img.height || 1) / (r.height || 1);
496
-
497
- const natX = Math.round(clamp(x - r.left, 0, r.width) * scaleX);
498
- const natY = Math.round(clamp(y - r.top, 0, r.height) * scaleY);
499
- const natW = Math.round(clamp(w, 0, r.width - (x - r.left)) * scaleX);
500
- const natH = Math.round(clamp(h, 0, r.height - (y - r.top)) * scaleY);
501
-
502
- if (natW <= 0 || natH <= 0) { this._close(false); return; }
503
-
504
- // Output dimensions = display-pixel crop size (preserve visual size)
505
- const outW = w;
506
- const outH = h;
507
-
508
- const naturalRect = /** @type {DOMRect} */ (/** @type {unknown} */ ({ x: natX, y: natY, width: natW, height: natH }));
509
- const canvas = await drawCropToCanvas(img, naturalRect, outW, outH);
510
-
511
- if (!canvas) {
512
- // Cross-origin failure — inform user and abort
513
- this._showCropError(
514
- 'Cannot crop this image: the image server does not allow cross-origin access. ' +
515
- 'Upload the image directly to use the crop tool.',
516
- );
517
- this._close(false);
518
- return;
519
- }
520
-
521
- // Determine output format: preserve JPEG, fall back to PNG
522
- const fmt = /^data:image\/(jpe?g)/i.exec(img.src) ? 'image/jpeg' : 'image/png';
523
- const quality = fmt === 'image/jpeg' ? 0.92 : undefined;
524
- const newSrc = canvas.toDataURL(fmt, quality);
525
-
526
- // Apply the crop
527
- this._close(false);
528
-
529
- img.src = newSrc;
530
- // Strip explicit width/height — the image should show at its new natural size
531
- img.style.width = '';
532
- img.style.height = '';
533
- img.removeAttribute('width');
534
- img.removeAttribute('height');
535
-
536
- this.context.invoke('editor.afterCommand');
537
- this.context.invoke('imageResizer.updateOverlay');
538
- }
539
-
540
- /**
541
- * Show a non-blocking inline error banner appended to document.body.
542
- * Auto-dismisses after 4 seconds.
543
- * @param {string} msg
544
- */
545
- _showCropError(msg) {
546
- const banner = document.createElement('div');
547
- banner.setAttribute('role', 'alert');
548
- banner.style.cssText = [
549
- 'position:fixed', 'bottom:24px', 'left:50%', 'transform:translateX(-50%)',
550
- 'z-index:10200', 'max-width:420px', 'width:max-content',
551
- 'background:#7f1d1d', 'color:#fecaca', 'border:1px solid #b91c1c',
552
- 'border-radius:8px', 'padding:12px 18px',
553
- 'font:13px/1.5 system-ui,sans-serif',
554
- 'box-shadow:0 4px 16px rgba(0,0,0,.4)',
555
- 'pointer-events:auto',
556
- ].join(';');
557
- banner.textContent = msg;
558
- document.body.appendChild(banner);
559
- setTimeout(() => { banner.remove(); }, 4000);
560
- }
561
-
562
- /**
563
- * Remove all overlay DOM elements and reset state.
564
- * @param {boolean} _committed - reserved for future use
565
- */
566
- _close(_committed) {
567
- this._dragDisposers?.();
568
- this._dragDisposers = null;
569
- this._disposers.forEach((d) => d());
570
- this._disposers = [];
571
-
572
- [this._scrim, this._cropBox, this._toolbar].forEach((el) => {
573
- el?.remove();
574
- });
575
-
576
- this._scrim = null;
577
- this._cropBox = null;
578
- this._toolbar = null;
579
- this._infoEl = null;
580
- this._handles = {};
581
- this._img = null;
582
- this._box = null;
583
- this._imgRect = null;
584
- this._arCheck = null;
585
- }
586
- }