@octanejs/image-crop 0.0.1

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.
@@ -0,0 +1,918 @@
1
+ import { type OctaneNode, useEffect, useRef, useState } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+
4
+ import type { Crop, Ords, PercentCrop, PixelCrop, XYOrds } from './types';
5
+ import {
6
+ areCropsEqual,
7
+ clamp,
8
+ cls,
9
+ containCrop,
10
+ convertToPercentCrop,
11
+ convertToPixelCrop,
12
+ defaultCrop,
13
+ nudgeCrop,
14
+ } from './utils';
15
+
16
+ import './ReactCrop.css';
17
+
18
+ interface EVData {
19
+ startClientX: number;
20
+ startClientY: number;
21
+ startCropX: number;
22
+ startCropY: number;
23
+ clientX: number;
24
+ clientY: number;
25
+ isResize: boolean;
26
+ ord?: Ords;
27
+ }
28
+
29
+ interface Rectangle {
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ }
35
+
36
+ type CSSProperties = Exclude<NonNullable<Octane.HTMLAttributes<Element>['style']>, string>;
37
+
38
+ const DOC_MOVE_OPTS = { capture: true, passive: false };
39
+ let instanceCount = 0;
40
+
41
+ const DEFAULT_ARIA_LABELS = {
42
+ cropArea: 'Use the arrow keys to move the crop selection area',
43
+ nwDragHandle: 'Use the arrow keys to move the north west drag handle to change the crop selection area',
44
+ nDragHandle: 'Use the up and down arrow keys to move the north drag handle to change the crop selection area',
45
+ neDragHandle: 'Use the arrow keys to move the north east drag handle to change the crop selection area',
46
+ eDragHandle: 'Use the up and down arrow keys to move the east drag handle to change the crop selection area',
47
+ seDragHandle: 'Use the arrow keys to move the south east drag handle to change the crop selection area',
48
+ sDragHandle: 'Use the up and down arrow keys to move the south drag handle to change the crop selection area',
49
+ swDragHandle: 'Use the arrow keys to move the south west drag handle to change the crop selection area',
50
+ wDragHandle: 'Use the up and down arrow keys to move the west drag handle to change the crop selection area',
51
+ };
52
+
53
+ export interface ReactCropProps {
54
+ /** An object of labels to override the built-in English ones */
55
+ ariaLabels?: {
56
+ cropArea: string;
57
+ nwDragHandle: string;
58
+ nDragHandle: string;
59
+ neDragHandle: string;
60
+ eDragHandle: string;
61
+ seDragHandle: string;
62
+ sDragHandle: string;
63
+ swDragHandle: string;
64
+ wDragHandle: string;
65
+ };
66
+ /** The aspect ratio of the crop, e.g. `1` for a square or `16 / 9` for landscape. */
67
+ aspect?: number;
68
+ /** Classes to pass to the `ReactCrop` element. */
69
+ className?: string;
70
+ /** The elements that you want to perform a crop on. For example
71
+ * an image or video. */
72
+ children?: OctaneNode;
73
+ /** Show the crop area as a circle. If your aspect is not 1 (a square) then the circle will be warped into an oval shape. Defaults to false. */
74
+ circularCrop?: boolean;
75
+ /** Since v10 all crop params are required except for aspect. Omit the entire crop object if you don't want a crop. See README on how to create an aspect crop with a % crop. */
76
+ crop?: Crop;
77
+ /** If true then the user cannot resize or draw a new crop. A class of `ReactCrop--disabled` is also added to the container for user styling. */
78
+ disabled?: boolean;
79
+ /** If true then the user cannot create or resize a crop, but can still drag the existing crop around. A class of `ReactCrop--locked` is also added to the container for user styling. */
80
+ locked?: boolean;
81
+ /** If true is passed then selection can't be disabled if the user clicks outside the selection area. */
82
+ keepSelection?: boolean;
83
+ /** A minimum crop width, in pixels. */
84
+ minWidth?: number;
85
+ /** A minimum crop height, in pixels. */
86
+ minHeight?: number;
87
+ /** A maximum crop width, in pixels. */
88
+ maxWidth?: number;
89
+ /** A maximum crop height, in pixels. */
90
+ maxHeight?: number;
91
+ /** A callback which happens for every change of the crop. You should set the crop to state and pass it back into the library via the `crop` prop. */
92
+ onChange: (crop: PixelCrop, percentageCrop: PercentCrop) => void;
93
+ /** A callback which happens after a resize, drag, or nudge. Passes the current crop state object in pixels and percent. */
94
+ onComplete?: (crop: PixelCrop, percentageCrop: PercentCrop) => void;
95
+ /** A callback which happens when a user starts dragging or resizing. It is convenient to manipulate elements outside this component. */
96
+ onDragStart?: (event: PointerEvent) => void;
97
+ /** A callback which happens when a user releases the cursor or touch after dragging or resizing. */
98
+ onDragEnd?: (event: PointerEvent) => void;
99
+ /** Render a custom element in crop selection. */
100
+ renderSelectionAddon?: (state: ReactCropState) => OctaneNode;
101
+ /** Show rule of thirds lines in the cropped area. Defaults to false. */
102
+ ruleOfThirds?: boolean;
103
+ /** Inline styles object to be passed to the `ReactCrop` element. */
104
+ style?: CSSProperties;
105
+ }
106
+
107
+ export interface ReactCropState {
108
+ cropIsActive: boolean;
109
+ newCropIsBeingDrawn: boolean;
110
+ }
111
+
112
+ export function ReactCrop(props: ReactCropProps): OctaneNode {
113
+ const propsRef = useRef(props);
114
+ propsRef.current = props;
115
+
116
+ const docMoveBoundRef = useRef(false);
117
+ const mouseDownOnCropRef = useRef(false);
118
+ const dragStartedRef = useRef(false);
119
+ const evDataRef = useRef<EVData>({
120
+ startClientX: 0,
121
+ startClientY: 0,
122
+ startCropX: 0,
123
+ startCropY: 0,
124
+ clientX: 0,
125
+ clientY: 0,
126
+ isResize: true,
127
+ });
128
+ const componentRef = useRef<HTMLDivElement | null>(null);
129
+ const mediaRef = useRef<HTMLDivElement | null>(null);
130
+ const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);
131
+ const initChangeCalledRef = useRef(false);
132
+ const instanceIdRef = useRef<string | null>(null);
133
+ const previousCropRef = useRef<Crop | undefined>(props.crop);
134
+ const docListenersRef = useRef<
135
+ | {
136
+ move: (event: PointerEvent) => void;
137
+ done: (event: PointerEvent) => void;
138
+ }
139
+ | null
140
+ >(null);
141
+
142
+ if (instanceIdRef.current === null) {
143
+ instanceIdRef.current = `rc-${instanceCount++}`;
144
+ }
145
+
146
+ const [cropIsActive, setCropIsActive] = useState(false);
147
+ const [newCropIsBeingDrawn, setNewCropIsBeingDrawn] = useState(false);
148
+
149
+ // We unfortunately get the bounding box every time as x+y changes
150
+ // due to scrolling.
151
+ function getBox(): Rectangle {
152
+ const element = mediaRef.current;
153
+ if (!element) {
154
+ return { x: 0, y: 0, width: 0, height: 0 };
155
+ }
156
+ const { x, y, width, height } = element.getBoundingClientRect();
157
+ return { x, y, width, height };
158
+ }
159
+
160
+ function bindDocMove(): void {
161
+ if (docMoveBoundRef.current) {
162
+ return;
163
+ }
164
+
165
+ const listeners = {
166
+ move: onDocPointerMove,
167
+ done: onDocPointerDone,
168
+ };
169
+ docListenersRef.current = listeners;
170
+
171
+ document.addEventListener('pointermove', listeners.move, DOC_MOVE_OPTS);
172
+ document.addEventListener('pointerup', listeners.done, DOC_MOVE_OPTS);
173
+ document.addEventListener('pointercancel', listeners.done, DOC_MOVE_OPTS);
174
+
175
+ docMoveBoundRef.current = true;
176
+ }
177
+
178
+ function unbindDocMove(): void {
179
+ if (!docMoveBoundRef.current) {
180
+ return;
181
+ }
182
+
183
+ const listeners = docListenersRef.current;
184
+ if (listeners) {
185
+ document.removeEventListener('pointermove', listeners.move, DOC_MOVE_OPTS);
186
+ document.removeEventListener('pointerup', listeners.done, DOC_MOVE_OPTS);
187
+ document.removeEventListener('pointercancel', listeners.done, DOC_MOVE_OPTS);
188
+ }
189
+
190
+ docListenersRef.current = null;
191
+ docMoveBoundRef.current = false;
192
+ }
193
+
194
+ function onCropPointerDown(event: PointerEvent): void {
195
+ const { crop, disabled } = propsRef.current;
196
+ const box = getBox();
197
+
198
+ if (!crop) {
199
+ return;
200
+ }
201
+
202
+ const pixelCrop = convertToPixelCrop(crop, box.width, box.height);
203
+
204
+ if (disabled) {
205
+ return;
206
+ }
207
+
208
+ if (event.cancelable) event.preventDefault(); // Stop drag selection.
209
+
210
+ // Bind to doc to follow movements outside of element.
211
+ bindDocMove();
212
+
213
+ // Focus for detecting keypress.
214
+ componentRef.current!.focus({ preventScroll: true });
215
+
216
+ const ord = (event.target as HTMLElement).dataset.ord as Ords;
217
+ const isResize = Boolean(ord);
218
+ let startClientX = event.clientX;
219
+ let startClientY = event.clientY;
220
+ let startCropX = pixelCrop.x;
221
+ let startCropY = pixelCrop.y;
222
+
223
+ // Set the starting coords to the opposite corner.
224
+ if (ord) {
225
+ const relativeX = event.clientX - box.x;
226
+ const relativeY = event.clientY - box.y;
227
+ let fromCornerX = 0;
228
+ let fromCornerY = 0;
229
+
230
+ if (ord === 'ne' || ord == 'e') {
231
+ fromCornerX = relativeX - (pixelCrop.x + pixelCrop.width);
232
+ fromCornerY = relativeY - pixelCrop.y;
233
+ startCropX = pixelCrop.x;
234
+ startCropY = pixelCrop.y + pixelCrop.height;
235
+ } else if (ord === 'se' || ord === 's') {
236
+ fromCornerX = relativeX - (pixelCrop.x + pixelCrop.width);
237
+ fromCornerY = relativeY - (pixelCrop.y + pixelCrop.height);
238
+ startCropX = pixelCrop.x;
239
+ startCropY = pixelCrop.y;
240
+ } else if (ord === 'sw' || ord == 'w') {
241
+ fromCornerX = relativeX - pixelCrop.x;
242
+ fromCornerY = relativeY - (pixelCrop.y + pixelCrop.height);
243
+ startCropX = pixelCrop.x + pixelCrop.width;
244
+ startCropY = pixelCrop.y;
245
+ } else if (ord === 'nw' || ord == 'n') {
246
+ fromCornerX = relativeX - pixelCrop.x;
247
+ fromCornerY = relativeY - pixelCrop.y;
248
+ startCropX = pixelCrop.x + pixelCrop.width;
249
+ startCropY = pixelCrop.y + pixelCrop.height;
250
+ }
251
+
252
+ startClientX = startCropX + box.x + fromCornerX;
253
+ startClientY = startCropY + box.y + fromCornerY;
254
+ }
255
+
256
+ evDataRef.current = {
257
+ startClientX,
258
+ startClientY,
259
+ startCropX,
260
+ startCropY,
261
+ clientX: event.clientX,
262
+ clientY: event.clientY,
263
+ isResize,
264
+ ord,
265
+ };
266
+
267
+ mouseDownOnCropRef.current = true;
268
+ setCropIsActive(true);
269
+ }
270
+
271
+ function onComponentPointerDown(event: PointerEvent): void {
272
+ const { crop, disabled, locked, keepSelection, onChange } = propsRef.current;
273
+ const box = getBox();
274
+
275
+ if (disabled || locked || keepSelection && crop) {
276
+ return;
277
+ }
278
+
279
+ if (event.cancelable) event.preventDefault(); // Stop drag selection.
280
+
281
+ // Bind to doc to follow movements outside of element.
282
+ bindDocMove();
283
+
284
+ // Focus for detecting keypress.
285
+ componentRef.current!.focus({ preventScroll: true });
286
+
287
+ const cropX = event.clientX - box.x;
288
+ const cropY = event.clientY - box.y;
289
+ const nextCrop: PixelCrop = {
290
+ unit: 'px',
291
+ x: cropX,
292
+ y: cropY,
293
+ width: 0,
294
+ height: 0,
295
+ };
296
+
297
+ evDataRef.current = {
298
+ startClientX: event.clientX,
299
+ startClientY: event.clientY,
300
+ startCropX: cropX,
301
+ startCropY: cropY,
302
+ clientX: event.clientX,
303
+ clientY: event.clientY,
304
+ isResize: true,
305
+ };
306
+
307
+ mouseDownOnCropRef.current = true;
308
+
309
+ onChange(
310
+ convertToPixelCrop(nextCrop, box.width, box.height),
311
+ convertToPercentCrop(nextCrop, box.width, box.height),
312
+ );
313
+
314
+ setCropIsActive(true);
315
+ setNewCropIsBeingDrawn(true);
316
+ }
317
+
318
+ function onDocPointerMove(event: PointerEvent): void {
319
+ const { crop, disabled, onChange, onDragStart } = propsRef.current;
320
+ const box = getBox();
321
+
322
+ if (disabled || !crop || !mouseDownOnCropRef.current) {
323
+ return;
324
+ }
325
+
326
+ // Stop drag selection.
327
+ if (event.cancelable) event.preventDefault();
328
+
329
+ if (!dragStartedRef.current) {
330
+ dragStartedRef.current = true;
331
+ if (onDragStart) {
332
+ onDragStart(event);
333
+ }
334
+ }
335
+
336
+ // Update pointer position.
337
+ const evData = evDataRef.current;
338
+
339
+ evData.clientX = event.clientX;
340
+ evData.clientY = event.clientY;
341
+
342
+ let nextCrop;
343
+
344
+ if (evData.isResize) {
345
+ nextCrop = resizeCrop();
346
+ } else {
347
+ nextCrop = dragCrop();
348
+ }
349
+
350
+ if (!areCropsEqual(crop, nextCrop)) {
351
+ onChange(
352
+ convertToPixelCrop(nextCrop, box.width, box.height),
353
+ convertToPercentCrop(nextCrop, box.width, box.height),
354
+ );
355
+ }
356
+ }
357
+
358
+ function onComponentKeyDown(event: KeyboardEvent): void {
359
+ const { crop, disabled, onChange, onComplete } = propsRef.current;
360
+
361
+ if (disabled) {
362
+ return;
363
+ }
364
+
365
+ const keyCode = event.key;
366
+ let nudged = false;
367
+
368
+ if (!crop) {
369
+ return;
370
+ }
371
+
372
+ const box = getBox();
373
+ const nextCrop = makePixelCrop(box);
374
+ const ctrlCmdPressed = navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey;
375
+ const nudgeStep =
376
+ ctrlCmdPressed
377
+ ? ReactCrop.nudgeStepLarge
378
+ : event.shiftKey
379
+ ? ReactCrop.nudgeStepMedium
380
+ : ReactCrop.nudgeStep;
381
+
382
+ if (keyCode === 'ArrowLeft') {
383
+ nextCrop.x -= nudgeStep;
384
+ nudged = true;
385
+ } else if (keyCode === 'ArrowRight') {
386
+ nextCrop.x += nudgeStep;
387
+ nudged = true;
388
+ } else if (keyCode === 'ArrowUp') {
389
+ nextCrop.y -= nudgeStep;
390
+ nudged = true;
391
+ } else if (keyCode === 'ArrowDown') {
392
+ nextCrop.y += nudgeStep;
393
+ nudged = true;
394
+ }
395
+
396
+ if (nudged) {
397
+ if (event.cancelable) event.preventDefault(); // Stop drag selection.
398
+
399
+ nextCrop.x = clamp(nextCrop.x, 0, box.width - nextCrop.width);
400
+ nextCrop.y = clamp(nextCrop.y, 0, box.height - nextCrop.height);
401
+
402
+ const pixelCrop = convertToPixelCrop(nextCrop, box.width, box.height);
403
+ const percentCrop = convertToPercentCrop(nextCrop, box.width, box.height);
404
+
405
+ onChange(pixelCrop, percentCrop);
406
+ if (onComplete) {
407
+ onComplete(pixelCrop, percentCrop);
408
+ }
409
+ }
410
+ }
411
+
412
+ function onHandlerKeyDown(event: KeyboardEvent, ord: Ords): void {
413
+ const {
414
+ aspect = 0,
415
+ crop,
416
+ disabled,
417
+ minWidth = 0,
418
+ minHeight = 0,
419
+ maxWidth,
420
+ maxHeight,
421
+ onChange,
422
+ onComplete,
423
+ } = propsRef.current;
424
+ const box = getBox();
425
+
426
+ if (disabled || !crop) {
427
+ return;
428
+ }
429
+
430
+ // Keep the event from bubbling up to the container
431
+ if (
432
+ event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' ||
433
+ event.key === 'ArrowRight'
434
+ ) {
435
+ event.stopPropagation();
436
+ event.preventDefault();
437
+ } else {
438
+ return;
439
+ }
440
+
441
+ const ctrlCmdPressed = navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey;
442
+ const offset =
443
+ ctrlCmdPressed
444
+ ? ReactCrop.nudgeStepLarge
445
+ : event.shiftKey
446
+ ? ReactCrop.nudgeStepMedium
447
+ : ReactCrop.nudgeStep;
448
+
449
+ const pixelCrop = convertToPixelCrop(crop, box.width, box.height);
450
+ const nudgedCrop = nudgeCrop(pixelCrop, event.key, offset, ord);
451
+ const containedCrop = containCrop(
452
+ nudgedCrop,
453
+ aspect,
454
+ ord,
455
+ box.width,
456
+ box.height,
457
+ minWidth,
458
+ minHeight,
459
+ maxWidth,
460
+ maxHeight,
461
+ );
462
+
463
+ if (!areCropsEqual(crop, containedCrop)) {
464
+ const percentCrop = convertToPercentCrop(containedCrop, box.width, box.height);
465
+ onChange(containedCrop, percentCrop);
466
+
467
+ if (onComplete) {
468
+ onComplete(containedCrop, percentCrop);
469
+ }
470
+ }
471
+ }
472
+
473
+ function onDocPointerDone(event: PointerEvent): void {
474
+ const { crop, disabled, onComplete, onDragEnd } = propsRef.current;
475
+ const box = getBox();
476
+
477
+ unbindDocMove();
478
+
479
+ if (disabled || !crop) {
480
+ return;
481
+ }
482
+
483
+ if (mouseDownOnCropRef.current) {
484
+ mouseDownOnCropRef.current = false;
485
+ dragStartedRef.current = false;
486
+
487
+ onDragEnd && onDragEnd(event);
488
+ onComplete &&
489
+ onComplete(
490
+ convertToPixelCrop(crop, box.width, box.height),
491
+ convertToPercentCrop(crop, box.width, box.height),
492
+ );
493
+
494
+ setCropIsActive(false);
495
+ setNewCropIsBeingDrawn(false);
496
+ }
497
+ }
498
+
499
+ function onDragFocus(): void {
500
+ // Fixes #491
501
+ componentRef.current?.scrollTo(0, 0);
502
+ }
503
+
504
+ function getCropStyle(): CSSProperties | undefined {
505
+ const { crop } = propsRef.current;
506
+
507
+ if (!crop) {
508
+ return undefined;
509
+ }
510
+
511
+ return {
512
+ top: `${crop.y}${crop.unit}`,
513
+ left: `${crop.x}${crop.unit}`,
514
+ width: `${crop.width}${crop.unit}`,
515
+ height: `${crop.height}${crop.unit}`,
516
+ };
517
+ }
518
+
519
+ function dragCrop(): PixelCrop {
520
+ const evData = evDataRef.current;
521
+ const box = getBox();
522
+ const nextCrop = makePixelCrop(box);
523
+ const xDiff = evData.clientX - evData.startClientX;
524
+ const yDiff = evData.clientY - evData.startClientY;
525
+
526
+ nextCrop.x = clamp(evData.startCropX + xDiff, 0, box.width - nextCrop.width);
527
+ nextCrop.y = clamp(evData.startCropY + yDiff, 0, box.height - nextCrop.height);
528
+
529
+ return nextCrop;
530
+ }
531
+
532
+ function getPointRegion(
533
+ box: Rectangle,
534
+ origOrd: Ords | undefined,
535
+ minWidth: number,
536
+ minHeight: number,
537
+ ): XYOrds {
538
+ const evData = evDataRef.current;
539
+
540
+ const relativeX = evData.clientX - box.x;
541
+ const relativeY = evData.clientY - box.y;
542
+
543
+ let topHalf: boolean;
544
+ if (minHeight && origOrd) {
545
+ // Uses orig ord (never flip when minHeight != 0)
546
+ topHalf = origOrd === 'nw' || origOrd === 'n' || origOrd === 'ne';
547
+ } else {
548
+ topHalf = relativeY < evData.startCropY;
549
+ }
550
+
551
+ let leftHalf: boolean;
552
+ if (minWidth && origOrd) {
553
+ // Uses orig ord (never flip when minWidth != 0)
554
+ leftHalf = origOrd === 'nw' || origOrd === 'w' || origOrd === 'sw';
555
+ } else {
556
+ leftHalf = relativeX < evData.startCropX;
557
+ }
558
+
559
+ if (leftHalf) {
560
+ return topHalf ? 'nw' : 'sw';
561
+ } else {
562
+ return topHalf ? 'ne' : 'se';
563
+ }
564
+ }
565
+
566
+ function resolveMinDimensions(
567
+ box: Rectangle,
568
+ aspect: number,
569
+ minWidth = 0,
570
+ minHeight = 0,
571
+ ): [number, number] {
572
+ const mw = Math.min(minWidth, box.width);
573
+ const mh = Math.min(minHeight, box.height);
574
+
575
+ if (!aspect || !mw && !mh) {
576
+ return [mw, mh];
577
+ }
578
+
579
+ if (aspect > 1) {
580
+ return mw ? [mw, mw / aspect] : [mh * aspect, mh];
581
+ } else {
582
+ return mh ? [mh * aspect, mh] : [mw, mw / aspect];
583
+ }
584
+ }
585
+
586
+ function resizeCrop(): PixelCrop {
587
+ const evData = evDataRef.current;
588
+ const { aspect = 0, maxWidth, maxHeight } = propsRef.current;
589
+ const box = getBox();
590
+ const [minWidth, minHeight] = resolveMinDimensions(
591
+ box,
592
+ aspect,
593
+ propsRef.current.minWidth,
594
+ propsRef.current.minHeight,
595
+ );
596
+ let nextCrop = makePixelCrop(box);
597
+ const area = getPointRegion(box, evData.ord, minWidth, minHeight);
598
+ const ord = evData.ord || area;
599
+ let xDiff = evData.clientX - evData.startClientX;
600
+ let yDiff = evData.clientY - evData.startClientY;
601
+
602
+ // When min dimensions are set, ensure crop isn't dragged when going
603
+ // beyond the other side #554
604
+ if (minWidth && ord === 'nw' || ord === 'w' || ord === 'sw') {
605
+ xDiff = Math.min(xDiff, -minWidth);
606
+ }
607
+
608
+ if (minHeight && ord === 'nw' || ord === 'n' || ord === 'ne') {
609
+ yDiff = Math.min(yDiff, -minHeight);
610
+ }
611
+
612
+ const tmpCrop: PixelCrop = {
613
+ unit: 'px',
614
+ x: 0,
615
+ y: 0,
616
+ width: 0,
617
+ height: 0,
618
+ };
619
+
620
+ if (area === 'ne') {
621
+ tmpCrop.x = evData.startCropX;
622
+ tmpCrop.width = xDiff;
623
+
624
+ if (aspect) {
625
+ tmpCrop.height = tmpCrop.width / aspect;
626
+ tmpCrop.y = evData.startCropY - tmpCrop.height;
627
+ } else {
628
+ tmpCrop.height = Math.abs(yDiff);
629
+ tmpCrop.y = evData.startCropY - tmpCrop.height;
630
+ }
631
+ } else if (area === 'se') {
632
+ tmpCrop.x = evData.startCropX;
633
+ tmpCrop.y = evData.startCropY;
634
+ tmpCrop.width = xDiff;
635
+
636
+ if (aspect) {
637
+ tmpCrop.height = tmpCrop.width / aspect;
638
+ } else {
639
+ tmpCrop.height = yDiff;
640
+ }
641
+ } else if (area === 'sw') {
642
+ tmpCrop.x = evData.startCropX + xDiff;
643
+ tmpCrop.y = evData.startCropY;
644
+ tmpCrop.width = Math.abs(xDiff);
645
+
646
+ if (aspect) {
647
+ tmpCrop.height = tmpCrop.width / aspect;
648
+ } else {
649
+ tmpCrop.height = yDiff;
650
+ }
651
+ } else if (area === 'nw') {
652
+ tmpCrop.x = evData.startCropX + xDiff;
653
+ tmpCrop.width = Math.abs(xDiff);
654
+
655
+ if (aspect) {
656
+ tmpCrop.height = tmpCrop.width / aspect;
657
+ tmpCrop.y = evData.startCropY - tmpCrop.height;
658
+ } else {
659
+ tmpCrop.height = Math.abs(yDiff);
660
+ tmpCrop.y = evData.startCropY + yDiff;
661
+ }
662
+ }
663
+
664
+ const containedCrop = containCrop(
665
+ tmpCrop,
666
+ aspect,
667
+ area,
668
+ box.width,
669
+ box.height,
670
+ minWidth,
671
+ minHeight,
672
+ maxWidth,
673
+ maxHeight,
674
+ );
675
+
676
+ // Apply x/y/width/height changes depending on ordinate
677
+ // (fixed aspect always applies both).
678
+ if (aspect || ReactCrop.xyOrds.indexOf(ord) > -1) {
679
+ nextCrop = containedCrop;
680
+ } else if (ReactCrop.xOrds.indexOf(ord) > -1) {
681
+ nextCrop.x = containedCrop.x;
682
+ nextCrop.width = containedCrop.width;
683
+ } else if (ReactCrop.yOrds.indexOf(ord) > -1) {
684
+ nextCrop.y = containedCrop.y;
685
+ nextCrop.height = containedCrop.height;
686
+ }
687
+
688
+ // When drawing a new crop with min dimensions we allow flipping, but
689
+ // ensure we don't flip outside the crop area, just ignore those.
690
+ nextCrop.x = clamp(nextCrop.x, 0, box.width - nextCrop.width);
691
+ nextCrop.y = clamp(nextCrop.y, 0, box.height - nextCrop.height);
692
+
693
+ return nextCrop;
694
+ }
695
+
696
+ function renderCropSelection(): OctaneNode {
697
+ const {
698
+ ariaLabels = DEFAULT_ARIA_LABELS,
699
+ disabled,
700
+ locked,
701
+ renderSelectionAddon,
702
+ ruleOfThirds,
703
+ crop,
704
+ } = propsRef.current;
705
+ const style = getCropStyle();
706
+
707
+ if (!crop) {
708
+ return undefined;
709
+ }
710
+
711
+ return <div
712
+ style={style}
713
+ className="ReactCrop__crop-selection"
714
+ onPointerDown={onCropPointerDown}
715
+ aria-label={ariaLabels.cropArea}
716
+ tabIndex={0}
717
+ onKeyDown={onComponentKeyDown}
718
+ role="group"
719
+ >
720
+ {!disabled && !locked
721
+ ? <div className="ReactCrop__drag-elements" onFocus={onDragFocus}>
722
+ <div className="ReactCrop__drag-bar ord-n" data-ord="n" />
723
+ <div className="ReactCrop__drag-bar ord-e" data-ord="e" />
724
+ <div className="ReactCrop__drag-bar ord-s" data-ord="s" />
725
+ <div className="ReactCrop__drag-bar ord-w" data-ord="w" />
726
+
727
+ <div
728
+ className="ReactCrop__drag-handle ord-nw"
729
+ data-ord="nw"
730
+ tabIndex={0}
731
+ aria-label={ariaLabels.nwDragHandle}
732
+ onKeyDown={(event) => onHandlerKeyDown(event, 'nw')}
733
+ role="button"
734
+ />
735
+ <div
736
+ className="ReactCrop__drag-handle ord-n"
737
+ data-ord="n"
738
+ tabIndex={0}
739
+ aria-label={ariaLabels.nDragHandle}
740
+ onKeyDown={(event) => onHandlerKeyDown(event, 'n')}
741
+ role="button"
742
+ />
743
+ <div
744
+ className="ReactCrop__drag-handle ord-ne"
745
+ data-ord="ne"
746
+ tabIndex={0}
747
+ aria-label={ariaLabels.neDragHandle}
748
+ onKeyDown={(event) => onHandlerKeyDown(event, 'ne')}
749
+ role="button"
750
+ />
751
+ <div
752
+ className="ReactCrop__drag-handle ord-e"
753
+ data-ord="e"
754
+ tabIndex={0}
755
+ aria-label={ariaLabels.eDragHandle}
756
+ onKeyDown={(event) => onHandlerKeyDown(event, 'e')}
757
+ role="button"
758
+ />
759
+ <div
760
+ className="ReactCrop__drag-handle ord-se"
761
+ data-ord="se"
762
+ tabIndex={0}
763
+ aria-label={ariaLabels.seDragHandle}
764
+ onKeyDown={(event) => onHandlerKeyDown(event, 'se')}
765
+ role="button"
766
+ />
767
+ <div
768
+ className="ReactCrop__drag-handle ord-s"
769
+ data-ord="s"
770
+ tabIndex={0}
771
+ aria-label={ariaLabels.sDragHandle}
772
+ onKeyDown={(event) => onHandlerKeyDown(event, 's')}
773
+ role="button"
774
+ />
775
+ <div
776
+ className="ReactCrop__drag-handle ord-sw"
777
+ data-ord="sw"
778
+ tabIndex={0}
779
+ aria-label={ariaLabels.swDragHandle}
780
+ onKeyDown={(event) => onHandlerKeyDown(event, 'sw')}
781
+ role="button"
782
+ />
783
+ <div
784
+ className="ReactCrop__drag-handle ord-w"
785
+ data-ord="w"
786
+ tabIndex={0}
787
+ aria-label={ariaLabels.wDragHandle}
788
+ onKeyDown={(event) => onHandlerKeyDown(event, 'w')}
789
+ role="button"
790
+ />
791
+ </div>
792
+ : undefined}
793
+ {renderSelectionAddon
794
+ ? <div
795
+ className="ReactCrop__selection-addon"
796
+ onPointerDown={(event) => event.stopPropagation()}
797
+ >{renderSelectionAddon({ cropIsActive, newCropIsBeingDrawn })}</div>
798
+ : undefined}
799
+ {ruleOfThirds
800
+ ? <>
801
+ <div className="ReactCrop__rule-of-thirds-hz" />
802
+ <div className="ReactCrop__rule-of-thirds-vt" />
803
+ </>
804
+ : undefined}
805
+ </div>;
806
+ }
807
+
808
+ function makePixelCrop(box: Rectangle): PixelCrop {
809
+ const crop = { ...defaultCrop, ...propsRef.current.crop || {} };
810
+ return convertToPixelCrop(crop, box.width, box.height);
811
+ }
812
+
813
+ useEffect(() => {
814
+ const previousCrop = previousCropRef.current;
815
+ previousCropRef.current = props.crop;
816
+
817
+ // Useful for when programatically setting a new
818
+ // crop and wanting to show a preview.
819
+ if (props.onComplete && !previousCrop && props.crop) {
820
+ const { width, height } = getBox();
821
+ if (width && height) {
822
+ props.onComplete(
823
+ convertToPixelCrop(props.crop, width, height),
824
+ convertToPercentCrop(props.crop, width, height),
825
+ );
826
+ }
827
+ }
828
+ }, [props.crop, props.onComplete]);
829
+
830
+ useEffect(() => {
831
+ return () => {
832
+ resizeObserverRef.current?.disconnect();
833
+ unbindDocMove();
834
+ };
835
+ }, []);
836
+
837
+ const {
838
+ aspect,
839
+ children,
840
+ circularCrop,
841
+ className,
842
+ crop,
843
+ disabled,
844
+ locked,
845
+ style,
846
+ ruleOfThirds,
847
+ } = props;
848
+ const cropSelection =
849
+ crop ? renderCropSelection() : null;
850
+
851
+ const componentClasses = cls(
852
+ 'ReactCrop',
853
+ className,
854
+ cropIsActive && 'ReactCrop--active',
855
+ disabled && 'ReactCrop--disabled',
856
+ locked && 'ReactCrop--locked',
857
+ newCropIsBeingDrawn && 'ReactCrop--new-crop',
858
+ crop && aspect && 'ReactCrop--fixed-aspect',
859
+ crop && circularCrop && 'ReactCrop--circular-crop',
860
+ crop && ruleOfThirds && 'ReactCrop--rule-of-thirds',
861
+ !dragStartedRef.current && crop && !crop.width && !crop.height && 'ReactCrop--invisible-crop',
862
+ circularCrop && 'ReactCrop--no-animate',
863
+ );
864
+
865
+ return <div ref={componentRef} className={componentClasses} style={style}>
866
+ <div
867
+ ref={mediaRef}
868
+ className="ReactCrop__child-wrapper"
869
+ onPointerDown={onComponentPointerDown}
870
+ >{children}</div>
871
+ {crop
872
+ ? <svg className="ReactCrop__crop-mask" width="100%" height="100%">
873
+ <defs>
874
+ <mask id={`hole-${instanceIdRef.current}`}>
875
+ <rect width="100%" height="100%" fill="white" />
876
+ {circularCrop
877
+ ? <ellipse
878
+ cx={`${crop.x + crop.width / 2}${crop.unit}`}
879
+ cy={`${crop.y + crop.height / 2}${crop.unit}`}
880
+ rx={`${crop.width / 2}${crop.unit}`}
881
+ ry={`${crop.height / 2}${crop.unit}`}
882
+ fill="black"
883
+ />
884
+ : <rect
885
+ x={`${crop.x}${crop.unit}`}
886
+ y={`${crop.y}${crop.unit}`}
887
+ width={`${crop.width}${crop.unit}`}
888
+ height={`${crop.height}${crop.unit}`}
889
+ fill="black"
890
+ />}
891
+ </mask>
892
+ </defs>
893
+ <rect
894
+ fill="black"
895
+ fillOpacity={0.5}
896
+ width="100%"
897
+ height="100%"
898
+ mask={`url(#hole-${instanceIdRef.current})`}
899
+ />
900
+ </svg>
901
+ : undefined}
902
+ {cropSelection}
903
+ </div>;
904
+ }
905
+
906
+ ReactCrop.xOrds = ['e', 'w'];
907
+ ReactCrop.yOrds = ['n', 's'];
908
+ ReactCrop.xyOrds = ['nw', 'ne', 'se', 'sw'];
909
+
910
+ ReactCrop.nudgeStep = 1;
911
+ ReactCrop.nudgeStepMedium = 10;
912
+ ReactCrop.nudgeStepLarge = 100;
913
+
914
+ ReactCrop.defaultProps = {
915
+ ariaLabels: DEFAULT_ARIA_LABELS,
916
+ };
917
+
918
+ export default ReactCrop;