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,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
- }