1 | import React, { PureComponent, createRef } from 'react';
|
2 | import clsx from 'clsx';
|
3 |
|
4 | import './ReactCrop.scss';
|
5 |
|
6 | const defaultCrop: Crop = {
|
7 | x: 0,
|
8 | y: 0,
|
9 | width: 0,
|
10 | height: 0,
|
11 | unit: 'px',
|
12 | };
|
13 |
|
14 | function clamp(num: number, min: number, max: number) {
|
15 | return Math.min(Math.max(num, min), max);
|
16 | }
|
17 |
|
18 | function isCropValid(crop: Partial<Crop>) {
|
19 | return crop && crop.width && !isNaN(crop.width) && crop.height && !isNaN(crop.height);
|
20 | }
|
21 |
|
22 | function makeAspectCrop(crop: Crop, imageWidth: number, imageHeight: number) {
|
23 | if (!crop.aspect || isNaN(crop.aspect)) {
|
24 | console.warn('`crop.aspect` should be a number in order to make an aspect crop', crop);
|
25 | return { ...defaultCrop, ...crop };
|
26 | }
|
27 |
|
28 | const completeCrop: Crop = {
|
29 | unit: 'px',
|
30 | x: crop.x || 0,
|
31 | y: crop.y || 0,
|
32 | width: crop.width || 0,
|
33 | height: crop.height || 0,
|
34 | aspect: crop.aspect,
|
35 | };
|
36 |
|
37 | if (crop.width) {
|
38 | completeCrop.height = completeCrop.width / crop.aspect;
|
39 | }
|
40 |
|
41 | if (crop.height) {
|
42 | completeCrop.width = completeCrop.height * crop.aspect;
|
43 | }
|
44 |
|
45 | if (completeCrop.y + completeCrop.height > imageHeight) {
|
46 | completeCrop.height = imageHeight - completeCrop.y;
|
47 | completeCrop.width = completeCrop.height * crop.aspect;
|
48 | }
|
49 |
|
50 | if (completeCrop.x + completeCrop.width > imageWidth) {
|
51 | completeCrop.width = imageWidth - completeCrop.x;
|
52 | completeCrop.height = completeCrop.width / crop.aspect;
|
53 | }
|
54 |
|
55 | return completeCrop;
|
56 | }
|
57 |
|
58 | function convertToPercentCrop(crop: Partial<Crop>, imageWidth: number, imageHeight: number): Crop {
|
59 | if (crop.unit === '%') {
|
60 | return { ...defaultCrop, ...crop };
|
61 | }
|
62 |
|
63 | return {
|
64 | unit: '%',
|
65 | aspect: crop.aspect,
|
66 | x: crop.x ? (crop.x / imageWidth) * 100 : 0,
|
67 | y: crop.y ? (crop.y / imageHeight) * 100 : 0,
|
68 | width: crop.width ? (crop.width / imageWidth) * 100 : 0,
|
69 | height: crop.height ? (crop.height / imageHeight) * 100 : 0,
|
70 | };
|
71 | }
|
72 |
|
73 | function convertToPixelCrop(crop: Partial<Crop>, imageWidth: number, imageHeight: number): Crop {
|
74 | if (!crop.unit) {
|
75 | return { ...defaultCrop, ...crop, unit: 'px' };
|
76 | }
|
77 |
|
78 | if (crop.unit === 'px') {
|
79 | return { ...defaultCrop, ...crop };
|
80 | }
|
81 |
|
82 | return {
|
83 | unit: 'px',
|
84 | aspect: crop.aspect,
|
85 | x: crop.x ? (crop.x * imageWidth) / 100 : 0,
|
86 | y: crop.y ? (crop.y * imageHeight) / 100 : 0,
|
87 | width: crop.width ? (crop.width * imageWidth) / 100 : 0,
|
88 | height: crop.height ? (crop.height * imageHeight) / 100 : 0,
|
89 | };
|
90 | }
|
91 |
|
92 | function resolveCrop(pixelCrop: Crop, imageWidth: number, imageHeight: number) {
|
93 | if (pixelCrop.aspect && (!pixelCrop.width || !pixelCrop.height)) {
|
94 | return makeAspectCrop(pixelCrop, imageWidth, imageHeight);
|
95 | }
|
96 |
|
97 | return pixelCrop;
|
98 | }
|
99 |
|
100 | function containCrop(prevCrop: Partial<Crop>, crop: Partial<Crop>, imageWidth: number, imageHeight: number) {
|
101 | const pixelCrop = convertToPixelCrop(crop, imageWidth, imageHeight);
|
102 | const prevPixelCrop = convertToPixelCrop(prevCrop, imageWidth, imageHeight);
|
103 |
|
104 |
|
105 | if (!pixelCrop.aspect) {
|
106 | if (pixelCrop.x < 0) {
|
107 | pixelCrop.x = 0;
|
108 | pixelCrop.width += pixelCrop.x;
|
109 | } else if (pixelCrop.x + pixelCrop.width > imageWidth) {
|
110 | pixelCrop.width = imageWidth - pixelCrop.x;
|
111 | }
|
112 |
|
113 | if (pixelCrop.y + pixelCrop.height > imageHeight) {
|
114 | pixelCrop.height = imageHeight - pixelCrop.y;
|
115 | }
|
116 |
|
117 | return pixelCrop;
|
118 | }
|
119 |
|
120 | let adjustedForX = false;
|
121 |
|
122 | if (pixelCrop.x < 0) {
|
123 | pixelCrop.x = 0;
|
124 | pixelCrop.width += pixelCrop.x;
|
125 | pixelCrop.height = pixelCrop.width / pixelCrop.aspect;
|
126 | adjustedForX = true;
|
127 | } else if (pixelCrop.x + pixelCrop.width > imageWidth) {
|
128 | pixelCrop.width = imageWidth - pixelCrop.x;
|
129 | pixelCrop.height = pixelCrop.width / pixelCrop.aspect;
|
130 | adjustedForX = true;
|
131 | }
|
132 |
|
133 |
|
134 |
|
135 | if (adjustedForX && prevPixelCrop.y > pixelCrop.y) {
|
136 | pixelCrop.y = pixelCrop.y + (pixelCrop.height - pixelCrop.height);
|
137 | }
|
138 |
|
139 | let adjustedForY = false;
|
140 |
|
141 | if (pixelCrop.y + pixelCrop.height > imageHeight) {
|
142 | pixelCrop.height = imageHeight - pixelCrop.y;
|
143 | pixelCrop.width = pixelCrop.height * pixelCrop.aspect;
|
144 | adjustedForY = true;
|
145 | }
|
146 |
|
147 |
|
148 |
|
149 | if (adjustedForY && prevPixelCrop.x > pixelCrop.x) {
|
150 | pixelCrop.x = pixelCrop.x + (pixelCrop.width - pixelCrop.width);
|
151 | }
|
152 |
|
153 | return pixelCrop;
|
154 | }
|
155 |
|
156 | const DOC_MOVE_OPTS = { capture: true, passive: false };
|
157 |
|
158 | type XOrds = 'e' | 'w';
|
159 | type YOrds = 'n' | 's';
|
160 | type XYOrds = 'nw' | 'ne' | 'se' | 'sw';
|
161 | type Ords = XOrds | YOrds | XYOrds;
|
162 |
|
163 | export interface Crop {
|
164 | aspect?: number;
|
165 | x: number;
|
166 | y: number;
|
167 | width: number;
|
168 | height: number;
|
169 | unit: 'px' | '%';
|
170 | }
|
171 |
|
172 | interface EVData {
|
173 | clientStartX: number;
|
174 | clientStartY: number;
|
175 | cropStartWidth: number;
|
176 | cropStartHeight: number;
|
177 | cropStartX: number;
|
178 | cropStartY: number;
|
179 | xDiff: number;
|
180 | yDiff: number;
|
181 | xInversed: boolean;
|
182 | yInversed: boolean;
|
183 | xCrossOver: boolean;
|
184 | yCrossOver: boolean;
|
185 | lastYCrossover: boolean;
|
186 | startXCrossOver: boolean;
|
187 | startYCrossOver: boolean;
|
188 | isResize: boolean;
|
189 | ord: Ords;
|
190 | }
|
191 |
|
192 | export interface ReactCropProps {
|
193 |
|
194 | className?: string;
|
195 |
|
196 | children?: React.ReactNode;
|
197 |
|
198 | circularCrop?: boolean;
|
199 |
|
200 | crop: Partial<Crop>;
|
201 |
|
202 | crossorigin?: React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>['crossOrigin'];
|
203 |
|
204 | disabled?: boolean;
|
205 |
|
206 | locked?: boolean;
|
207 |
|
208 | imageAlt?: string;
|
209 |
|
210 | imageStyle?: React.CSSProperties;
|
211 |
|
212 | keepSelection?: boolean;
|
213 |
|
214 | minWidth?: number;
|
215 |
|
216 | minHeight?: number;
|
217 |
|
218 | maxWidth?: number;
|
219 |
|
220 | maxHeight?: number;
|
221 |
|
222 | onChange: (crop: Crop, percentageCrop: Crop) => void;
|
223 |
|
224 | onComplete?: (crop: Crop, percentageCrop: Crop) => void;
|
225 |
|
226 | onImageError?: React.DOMAttributes<HTMLImageElement>['onError'];
|
227 |
|
228 | onImageLoaded?: (image: HTMLImageElement) => void | boolean;
|
229 |
|
230 | onDragStart?: (e: PointerEvent) => void;
|
231 |
|
232 | onDragEnd?: (e: PointerEvent) => void;
|
233 |
|
234 | renderComponent?: React.ReactNode;
|
235 |
|
236 | renderSelectionAddon?: (state: ReactCropState) => React.ReactNode;
|
237 |
|
238 | rotate?: number;
|
239 |
|
240 | ruleOfThirds?: boolean;
|
241 |
|
242 | scale?: number;
|
243 |
|
244 | src: string;
|
245 |
|
246 | style?: React.CSSProperties;
|
247 |
|
248 | zoom?: number;
|
249 |
|
250 | spin?: number;
|
251 | }
|
252 |
|
253 | export interface ReactCropState {
|
254 | cropIsActive: boolean;
|
255 | newCropIsBeingDrawn: boolean;
|
256 | }
|
257 |
|
258 | class ReactCrop extends PureComponent<ReactCropProps, ReactCropState> {
|
259 | static xOrds = ['e', 'w'];
|
260 | static yOrds = ['n', 's'];
|
261 | static xyOrds = ['nw', 'ne', 'se', 'sw'];
|
262 |
|
263 | static nudgeStep = 1;
|
264 | static nudgeStepMedium = 10;
|
265 | static nudgeStepLarge = 100;
|
266 |
|
267 | keysDown = new Set<string>();
|
268 | docMoveBound = false;
|
269 | mouseDownOnCrop = false;
|
270 | dragStarted = false;
|
271 | evData: EVData = {
|
272 | clientStartX: 0,
|
273 | clientStartY: 0,
|
274 | cropStartWidth: 0,
|
275 | cropStartHeight: 0,
|
276 | cropStartX: 0,
|
277 | cropStartY: 0,
|
278 | xDiff: 0,
|
279 | yDiff: 0,
|
280 | xInversed: false,
|
281 | yInversed: false,
|
282 | xCrossOver: false,
|
283 | yCrossOver: false,
|
284 | lastYCrossover: false,
|
285 | startXCrossOver: false,
|
286 | startYCrossOver: false,
|
287 | isResize: true,
|
288 | ord: 'nw',
|
289 | };
|
290 |
|
291 | componentRef = createRef<HTMLDivElement>();
|
292 | mediaWrapperRef = createRef<HTMLDivElement>();
|
293 | imageRef = createRef<HTMLImageElement>();
|
294 | cropSelectRef = createRef<HTMLDivElement>();
|
295 |
|
296 | state: ReactCropState = {
|
297 | cropIsActive: false,
|
298 | newCropIsBeingDrawn: false,
|
299 | };
|
300 |
|
301 | componentDidMount() {
|
302 | if (this.componentRef.current) {
|
303 | this.componentRef.current.addEventListener('medialoaded', this.onMediaLoaded);
|
304 | }
|
305 | }
|
306 |
|
307 | componentWillUnmount() {
|
308 | if (this.componentRef.current) {
|
309 | this.componentRef.current.removeEventListener('medialoaded', this.onMediaLoaded);
|
310 | }
|
311 | }
|
312 |
|
313 | componentDidUpdate(prevProps: ReactCropProps) {
|
314 | const { crop, onChange, onComplete } = this.props;
|
315 |
|
316 | if (
|
317 | this.imageRef.current &&
|
318 | crop &&
|
319 | prevProps.crop !== crop &&
|
320 | crop.aspect &&
|
321 | ((crop.width && !crop.height) || (!crop.width && crop.height))
|
322 | ) {
|
323 | const { width, height } = this.imageRef.current;
|
324 | const newCrop = this.makeNewCrop();
|
325 | const completedCrop = makeAspectCrop(newCrop, width, height);
|
326 |
|
327 | const pixelCrop = convertToPixelCrop(completedCrop, width, height);
|
328 | const percentCrop = convertToPercentCrop(completedCrop, width, height);
|
329 | onChange(pixelCrop, percentCrop);
|
330 |
|
331 | if (onComplete) {
|
332 | onComplete(pixelCrop, percentCrop);
|
333 | }
|
334 | }
|
335 | }
|
336 |
|
337 | bindDocMove() {
|
338 | if (this.docMoveBound) {
|
339 | return;
|
340 | }
|
341 |
|
342 | document.addEventListener('pointermove', this.onDocPointerMove, DOC_MOVE_OPTS);
|
343 | document.addEventListener('pointerup', this.onDocPointerDone, DOC_MOVE_OPTS);
|
344 | document.addEventListener('pointercancel', this.onDocPointerDone, DOC_MOVE_OPTS);
|
345 |
|
346 | this.docMoveBound = true;
|
347 | }
|
348 |
|
349 | unbindDocMove() {
|
350 | if (!this.docMoveBound) {
|
351 | return;
|
352 | }
|
353 |
|
354 | document.removeEventListener('pointermove', this.onDocPointerMove, DOC_MOVE_OPTS);
|
355 | document.removeEventListener('pointerup', this.onDocPointerDone, DOC_MOVE_OPTS);
|
356 | document.removeEventListener('pointercancel', this.onDocPointerDone, DOC_MOVE_OPTS);
|
357 |
|
358 | this.docMoveBound = false;
|
359 | }
|
360 |
|
361 | onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
362 | const { crop, disabled } = this.props;
|
363 | const { width, height } = this.mediaDimensions;
|
364 | const pixelCrop = convertToPixelCrop(crop, width, height);
|
365 |
|
366 | if (disabled) {
|
367 | return;
|
368 | }
|
369 |
|
370 | if (e.cancelable) e.preventDefault();
|
371 |
|
372 |
|
373 | this.bindDocMove();
|
374 |
|
375 |
|
376 | (this.componentRef.current as HTMLDivElement).focus({ preventScroll: true });
|
377 |
|
378 | const { ord } = (e.target as HTMLElement).dataset;
|
379 | const xInversed = ord === 'nw' || ord === 'w' || ord === 'sw';
|
380 | const yInversed = ord === 'nw' || ord === 'n' || ord === 'ne';
|
381 |
|
382 | this.evData = {
|
383 | clientStartX: e.clientX,
|
384 | clientStartY: e.clientY,
|
385 | cropStartWidth: pixelCrop.width,
|
386 | cropStartHeight: pixelCrop.height,
|
387 | cropStartX: xInversed ? pixelCrop.x + pixelCrop.width : pixelCrop.x,
|
388 | cropStartY: yInversed ? pixelCrop.y + pixelCrop.height : pixelCrop.y,
|
389 | xDiff: 0,
|
390 | yDiff: 0,
|
391 | xInversed,
|
392 | yInversed,
|
393 | xCrossOver: xInversed,
|
394 | yCrossOver: yInversed,
|
395 | lastYCrossover: yInversed,
|
396 | startXCrossOver: xInversed,
|
397 | startYCrossOver: yInversed,
|
398 | isResize: Boolean(ord),
|
399 | ord: (ord || 'ne') as Ords,
|
400 | };
|
401 |
|
402 | this.mouseDownOnCrop = true;
|
403 | this.setState({ cropIsActive: true });
|
404 | };
|
405 |
|
406 | onComponentPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
407 | const { crop, disabled, locked, keepSelection, onChange, zoom = 1, spin = 0 } = this.props;
|
408 |
|
409 | const componentEl = (this.mediaWrapperRef.current as HTMLDivElement).firstChild;
|
410 |
|
411 | if (e.target !== componentEl || !componentEl.contains(e.target as Node)) {
|
412 | return;
|
413 | }
|
414 |
|
415 | if (disabled || locked || (keepSelection && isCropValid(crop))) {
|
416 | return;
|
417 | }
|
418 |
|
419 | if (e.cancelable) e.preventDefault();
|
420 |
|
421 |
|
422 | this.bindDocMove();
|
423 |
|
424 |
|
425 | (this.componentRef.current as HTMLDivElement).focus({ preventScroll: true });
|
426 |
|
427 | const rect = (this.mediaWrapperRef.current as HTMLDivElement).getBoundingClientRect();
|
428 | let x = 0;
|
429 | let y = 0;
|
430 | let scaledX = (e.clientX - rect.left) / zoom;
|
431 | let scaledY = (e.clientY - rect.top) / zoom;
|
432 | let degrees = spin;
|
433 | let radians = Math.abs((degrees * Math.PI) / 180.0);
|
434 | if ((degrees > -45 && degrees <= 45) || (Math.abs(degrees) > 135 && Math.abs(degrees) <= 180)) {
|
435 |
|
436 | x = scaledX * Math.cos(radians);
|
437 | y = scaledY * Math.cos(radians);
|
438 | } else if (degrees > 45 && degrees <= 135) {
|
439 |
|
440 | x = scaledY * Math.sin(radians);
|
441 | y = scaledX * Math.sin(radians) * -1;
|
442 | } else if (degrees > -135 && degrees <= -45) {
|
443 |
|
444 | x = scaledY * Math.sin(radians) * -1;
|
445 | y = scaledX * Math.sin(radians);
|
446 | }
|
447 |
|
448 | const nextCrop: Crop = {
|
449 | unit: 'px',
|
450 | aspect: crop ? crop.aspect : undefined,
|
451 | x,
|
452 | y,
|
453 | width: 0,
|
454 | height: 0,
|
455 | };
|
456 |
|
457 | this.evData = {
|
458 | clientStartX: e.clientX,
|
459 | clientStartY: e.clientY,
|
460 | cropStartWidth: nextCrop.width,
|
461 | cropStartHeight: nextCrop.height,
|
462 | cropStartX: nextCrop.x,
|
463 | cropStartY: nextCrop.y,
|
464 | xDiff: 0,
|
465 | yDiff: 0,
|
466 | xInversed: false,
|
467 | yInversed: false,
|
468 | xCrossOver: false,
|
469 | yCrossOver: false,
|
470 | lastYCrossover: false,
|
471 | startXCrossOver: false,
|
472 | startYCrossOver: false,
|
473 | isResize: true,
|
474 | ord: 'nw',
|
475 | };
|
476 |
|
477 | this.mouseDownOnCrop = true;
|
478 |
|
479 | const { width, height } = this.mediaDimensions;
|
480 |
|
481 | onChange(convertToPixelCrop(nextCrop, width, height), convertToPercentCrop(nextCrop, width, height));
|
482 |
|
483 | this.setState({ cropIsActive: true, newCropIsBeingDrawn: true });
|
484 | };
|
485 |
|
486 | onDocPointerMove = (e: PointerEvent) => {
|
487 | const { crop, disabled, onChange, onDragStart, zoom = 1, spin = 0 } = this.props;
|
488 |
|
489 | if (disabled) {
|
490 | return;
|
491 | }
|
492 |
|
493 | if (!this.mouseDownOnCrop) {
|
494 | return;
|
495 | }
|
496 |
|
497 | if (e.cancelable) e.preventDefault();
|
498 |
|
499 | if (!this.dragStarted) {
|
500 | this.dragStarted = true;
|
501 | if (onDragStart) {
|
502 | onDragStart(e);
|
503 | }
|
504 | }
|
505 |
|
506 | const { evData } = this;
|
507 |
|
508 | let scaledX = (e.clientX - evData.clientStartX) / zoom;
|
509 | let scaledY = (e.clientY - evData.clientStartY) / zoom;
|
510 | let degrees = spin;
|
511 | let radians = Math.abs((degrees * Math.PI) / 180.0);
|
512 | if ((degrees > -45 && degrees <= 45) || (Math.abs(degrees) > 135 && Math.abs(degrees) <= 180)) {
|
513 |
|
514 | evData.xDiff = scaledX * Math.cos(radians);
|
515 | evData.yDiff = scaledY * Math.cos(radians);
|
516 | } else if (degrees > 45 && degrees <= 135) {
|
517 |
|
518 | evData.xDiff = scaledY * Math.sin(radians);
|
519 | evData.yDiff = scaledX * Math.sin(radians) * -1;
|
520 | } else if (degrees > -135 && degrees <= -45) {
|
521 |
|
522 | evData.xDiff = scaledY * Math.sin(radians) * -1;
|
523 | evData.yDiff = scaledX * Math.sin(radians);
|
524 | }
|
525 |
|
526 | let nextCrop;
|
527 |
|
528 | if (evData.isResize) {
|
529 | nextCrop = this.resizeCrop();
|
530 | } else {
|
531 | nextCrop = this.dragCrop();
|
532 | }
|
533 |
|
534 | if (nextCrop !== crop) {
|
535 | const { width, height } = this.mediaDimensions;
|
536 | onChange(convertToPixelCrop(nextCrop, width, height), convertToPercentCrop(nextCrop, width, height));
|
537 | }
|
538 | };
|
539 |
|
540 | onComponentKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
541 | const { crop, disabled, onChange, onComplete } = this.props;
|
542 |
|
543 | if (disabled) {
|
544 | return;
|
545 | }
|
546 |
|
547 | this.keysDown.add(e.key);
|
548 | let nudged = false;
|
549 |
|
550 | if (!isCropValid(crop)) {
|
551 | return;
|
552 | }
|
553 |
|
554 | const nextCrop = this.makeNewCrop();
|
555 | const ctrlCmdPressed = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey;
|
556 | const nudgeStep = ctrlCmdPressed
|
557 | ? ReactCrop.nudgeStepLarge
|
558 | : e.shiftKey
|
559 | ? ReactCrop.nudgeStepMedium
|
560 | : ReactCrop.nudgeStep;
|
561 |
|
562 | if (this.keysDown.has('ArrowLeft')) {
|
563 | nextCrop.x -= nudgeStep;
|
564 | nudged = true;
|
565 | }
|
566 |
|
567 | if (this.keysDown.has('ArrowRight')) {
|
568 | nextCrop.x += nudgeStep;
|
569 | nudged = true;
|
570 | }
|
571 |
|
572 | if (this.keysDown.has('ArrowUp')) {
|
573 | nextCrop.y -= nudgeStep;
|
574 | nudged = true;
|
575 | }
|
576 |
|
577 | if (this.keysDown.has('ArrowDown')) {
|
578 | nextCrop.y += nudgeStep;
|
579 | nudged = true;
|
580 | }
|
581 |
|
582 | if (nudged) {
|
583 | if (e.cancelable) e.preventDefault();
|
584 | const { width, height } = this.mediaDimensions;
|
585 |
|
586 | nextCrop.x = clamp(nextCrop.x, 0, width - nextCrop.width);
|
587 | nextCrop.y = clamp(nextCrop.y, 0, height - nextCrop.height);
|
588 |
|
589 | const pixelCrop = convertToPixelCrop(nextCrop, width, height);
|
590 | const percentCrop = convertToPercentCrop(nextCrop, width, height);
|
591 |
|
592 | onChange(pixelCrop, percentCrop);
|
593 | if (onComplete) {
|
594 | onComplete(pixelCrop, percentCrop);
|
595 | }
|
596 | }
|
597 | };
|
598 |
|
599 | onComponentKeyUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
600 | this.keysDown.delete(e.key);
|
601 | };
|
602 |
|
603 | onDocPointerDone = (e: PointerEvent) => {
|
604 | const { crop, disabled, onComplete, onDragEnd } = this.props;
|
605 |
|
606 | this.unbindDocMove();
|
607 |
|
608 | if (disabled) {
|
609 | return;
|
610 | }
|
611 |
|
612 | if (this.mouseDownOnCrop) {
|
613 | this.mouseDownOnCrop = false;
|
614 | this.dragStarted = false;
|
615 |
|
616 | const { width, height } = this.mediaDimensions;
|
617 |
|
618 | onDragEnd && onDragEnd(e);
|
619 | onComplete && onComplete(convertToPixelCrop(crop, width, height), convertToPercentCrop(crop, width, height));
|
620 |
|
621 | this.setState({ cropIsActive: false, newCropIsBeingDrawn: false });
|
622 | }
|
623 | };
|
624 |
|
625 |
|
626 |
|
627 | createNewCrop() {
|
628 | const { width, height } = this.mediaDimensions;
|
629 | const crop = this.makeNewCrop();
|
630 | const resolvedCrop = resolveCrop(crop, width, height);
|
631 | const pixelCrop = convertToPixelCrop(resolvedCrop, width, height);
|
632 | const percentCrop = convertToPercentCrop(resolvedCrop, width, height);
|
633 | return { pixelCrop, percentCrop };
|
634 | }
|
635 |
|
636 |
|
637 |
|
638 | onMediaLoaded = () => {
|
639 | const { onComplete, onChange } = this.props;
|
640 | const { pixelCrop, percentCrop } = this.createNewCrop();
|
641 | onChange(pixelCrop, percentCrop);
|
642 | onComplete && onComplete(pixelCrop, percentCrop);
|
643 | };
|
644 |
|
645 | onImageLoad = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
646 | const { onComplete, onChange, onImageLoaded } = this.props;
|
647 |
|
648 |
|
649 |
|
650 | const res = onImageLoaded ? onImageLoaded(e.currentTarget) : true;
|
651 |
|
652 | if (res !== false) {
|
653 | const { pixelCrop, percentCrop } = this.createNewCrop();
|
654 | onChange(pixelCrop, percentCrop);
|
655 | onComplete && onComplete(pixelCrop, percentCrop);
|
656 | }
|
657 | };
|
658 |
|
659 | get mediaDimensions() {
|
660 | let width = 0;
|
661 | let height = 0;
|
662 | if (this.mediaWrapperRef.current) {
|
663 | width = this.mediaWrapperRef.current.clientWidth;
|
664 | height = this.mediaWrapperRef.current.clientHeight;
|
665 | }
|
666 | return { width, height };
|
667 | }
|
668 |
|
669 | getCropStyle() {
|
670 | const crop = this.makeNewCrop(this.props.crop ? this.props.crop.unit : 'px');
|
671 |
|
672 | return {
|
673 | top: `${crop.y}${crop.unit}`,
|
674 | left: `${crop.x}${crop.unit}`,
|
675 | width: `${crop.width}${crop.unit}`,
|
676 | height: `${crop.height}${crop.unit}`,
|
677 | };
|
678 | }
|
679 |
|
680 | getNewSize() {
|
681 | const { crop, minWidth = 0, maxWidth, minHeight = 0, maxHeight } = this.props;
|
682 | const { evData } = this;
|
683 | const { width, height } = this.mediaDimensions;
|
684 |
|
685 |
|
686 | let newWidth = evData.cropStartWidth + evData.xDiff;
|
687 |
|
688 | if (evData.xCrossOver) {
|
689 | newWidth = Math.abs(newWidth);
|
690 | }
|
691 |
|
692 | newWidth = clamp(newWidth, minWidth, maxWidth || width);
|
693 |
|
694 |
|
695 | let newHeight;
|
696 |
|
697 | if (crop.aspect) {
|
698 | newHeight = newWidth / crop.aspect;
|
699 | } else {
|
700 | newHeight = evData.cropStartHeight + evData.yDiff;
|
701 | }
|
702 |
|
703 | if (evData.yCrossOver) {
|
704 |
|
705 | newHeight = Math.min(Math.abs(newHeight), evData.cropStartY);
|
706 | }
|
707 |
|
708 | newHeight = clamp(newHeight, minHeight, maxHeight || height);
|
709 |
|
710 | if (crop.aspect) {
|
711 | newWidth = clamp(newHeight * crop.aspect, 0, width);
|
712 | }
|
713 |
|
714 | return {
|
715 | width: newWidth,
|
716 | height: newHeight,
|
717 | };
|
718 | }
|
719 |
|
720 | dragCrop() {
|
721 | const nextCrop = this.makeNewCrop();
|
722 | const { evData } = this;
|
723 | const { width, height } = this.mediaDimensions;
|
724 |
|
725 | nextCrop.x = clamp(evData.cropStartX + evData.xDiff, 0, width - nextCrop.width);
|
726 | nextCrop.y = clamp(evData.cropStartY + evData.yDiff, 0, height - nextCrop.height);
|
727 |
|
728 | return nextCrop;
|
729 | }
|
730 |
|
731 | resizeCrop() {
|
732 | const { evData } = this;
|
733 | const { crop, minWidth = 0, minHeight = 0 } = this.props;
|
734 | const nextCrop = this.makeNewCrop();
|
735 | const { ord } = evData;
|
736 |
|
737 |
|
738 |
|
739 | if (evData.xInversed) {
|
740 | evData.xDiff -= evData.cropStartWidth * 2;
|
741 | }
|
742 | if (evData.yInversed) {
|
743 | evData.yDiff -= evData.cropStartHeight * 2;
|
744 | }
|
745 |
|
746 |
|
747 | const newSize = this.getNewSize();
|
748 |
|
749 |
|
750 |
|
751 | let newX = evData.cropStartX;
|
752 | let newY = evData.cropStartY;
|
753 |
|
754 | if (evData.xCrossOver) {
|
755 | newX = nextCrop.x + (nextCrop.width - newSize.width);
|
756 | }
|
757 |
|
758 | if (evData.yCrossOver) {
|
759 |
|
760 |
|
761 | if (evData.lastYCrossover === false) {
|
762 | newY = nextCrop.y - newSize.height;
|
763 | } else {
|
764 | newY = nextCrop.y + (nextCrop.height - newSize.height);
|
765 | }
|
766 | }
|
767 |
|
768 | const { width, height } = this.mediaDimensions;
|
769 |
|
770 | const containedCrop = containCrop(
|
771 | this.props.crop,
|
772 | {
|
773 | unit: nextCrop.unit,
|
774 | x: newX,
|
775 | y: newY,
|
776 | width: newSize.width,
|
777 | height: newSize.height,
|
778 | aspect: nextCrop.aspect,
|
779 | },
|
780 | width,
|
781 | height
|
782 | );
|
783 |
|
784 |
|
785 | if (nextCrop.aspect || ReactCrop.xyOrds.indexOf(ord) > -1) {
|
786 | nextCrop.x = containedCrop.x;
|
787 | nextCrop.y = containedCrop.y;
|
788 | nextCrop.width = containedCrop.width;
|
789 | nextCrop.height = containedCrop.height;
|
790 | } else if (ReactCrop.xOrds.indexOf(ord) > -1) {
|
791 | nextCrop.x = containedCrop.x;
|
792 | nextCrop.width = containedCrop.width;
|
793 | } else if (ReactCrop.yOrds.indexOf(ord) > -1) {
|
794 | nextCrop.y = containedCrop.y;
|
795 | nextCrop.height = containedCrop.height;
|
796 | }
|
797 |
|
798 | evData.lastYCrossover = evData.yCrossOver;
|
799 | this.crossOverCheck();
|
800 |
|
801 |
|
802 |
|
803 | if (nextCrop.width < minWidth) {
|
804 | return crop;
|
805 | }
|
806 | if (nextCrop.height < minHeight) {
|
807 | return crop;
|
808 | }
|
809 |
|
810 | return nextCrop;
|
811 | }
|
812 |
|
813 | getRotatedCursor(handle: string, degrees: number) {
|
814 | if ((degrees > -135 && degrees <= -45) || (degrees > 45 && degrees <= 135)) {
|
815 |
|
816 | switch (handle) {
|
817 | case 'nw':
|
818 | return { cursor: 'ne-resize' };
|
819 | case 'n':
|
820 | return { cursor: 'w-resize' };
|
821 | case 'ne':
|
822 | return { cursor: 'nw-resize' };
|
823 | case 'e':
|
824 | return { cursor: 's-resize' };
|
825 | case 'se':
|
826 | return { cursor: 'sw-resize' };
|
827 | case 's':
|
828 | return { cursor: 'e-resize' };
|
829 | case 'sw':
|
830 | return { cursor: 'se-resize' };
|
831 | case 'w':
|
832 | return { cursor: 'n-resize' };
|
833 | }
|
834 | }
|
835 | }
|
836 |
|
837 | createCropSelection() {
|
838 | const { disabled, locked, renderSelectionAddon, ruleOfThirds, crop, spin = 0 } = this.props;
|
839 | const style = this.getCropStyle();
|
840 |
|
841 | return (
|
842 | <div style={style} className="ReactCrop__crop-selection" onPointerDown={this.onCropPointerDown}>
|
843 | {!disabled && !locked && (
|
844 | <div className="ReactCrop__drag-elements">
|
845 | <div className="ReactCrop__drag-bar ord-n" data-ord="n" />
|
846 | <div className="ReactCrop__drag-bar ord-e" data-ord="e" />
|
847 | <div className="ReactCrop__drag-bar ord-s" data-ord="s" />
|
848 | <div className="ReactCrop__drag-bar ord-w" data-ord="w" />
|
849 |
|
850 | <div className="ReactCrop__drag-handle ord-nw" data-ord="nw" style={this.getRotatedCursor('nw', spin)} />
|
851 | <div className="ReactCrop__drag-handle ord-n" data-ord="n" style={this.getRotatedCursor('n', spin)} />
|
852 | <div className="ReactCrop__drag-handle ord-ne" data-ord="ne" style={this.getRotatedCursor('ne', spin)} />
|
853 | <div className="ReactCrop__drag-handle ord-e" data-ord="e" style={this.getRotatedCursor('e', spin)} />
|
854 | <div className="ReactCrop__drag-handle ord-se" data-ord="se" style={this.getRotatedCursor('se', spin)} />
|
855 | <div className="ReactCrop__drag-handle ord-s" data-ord="s" style={this.getRotatedCursor('s', spin)} />
|
856 | <div className="ReactCrop__drag-handle ord-sw" data-ord="sw" style={this.getRotatedCursor('sw', spin)} />
|
857 | <div className="ReactCrop__drag-handle ord-w" data-ord="w" style={this.getRotatedCursor('w', spin)} />
|
858 | </div>
|
859 | )}
|
860 | {renderSelectionAddon && isCropValid(crop) && (
|
861 | <div className="ReactCrop__selection-addon" onMouseDown={e => e.stopPropagation()}>
|
862 | {renderSelectionAddon(this.state)}
|
863 | </div>
|
864 | )}
|
865 | {ruleOfThirds && (
|
866 | <>
|
867 | <div className="ReactCrop__rule-of-thirds-hz" />
|
868 | <div className="ReactCrop__rule-of-thirds-vt" />
|
869 | </>
|
870 | )}
|
871 | </div>
|
872 | );
|
873 | }
|
874 |
|
875 | makeNewCrop(unit = 'px') {
|
876 | const crop = { ...defaultCrop, ...(this.props.crop || {}) };
|
877 | const { width, height } = this.mediaDimensions;
|
878 |
|
879 | return unit === 'px' ? convertToPixelCrop(crop, width, height) : convertToPercentCrop(crop, width, height);
|
880 | }
|
881 |
|
882 | crossOverCheck() {
|
883 | const { evData } = this;
|
884 | const { minWidth, minHeight } = this.props;
|
885 |
|
886 | if (
|
887 | !minWidth &&
|
888 | ((!evData.xCrossOver && -Math.abs(evData.cropStartWidth) - evData.xDiff >= 0) ||
|
889 | (evData.xCrossOver && -Math.abs(evData.cropStartWidth) - evData.xDiff <= 0))
|
890 | ) {
|
891 | evData.xCrossOver = !evData.xCrossOver;
|
892 | }
|
893 |
|
894 | if (
|
895 | !minHeight &&
|
896 | ((!evData.yCrossOver && -Math.abs(evData.cropStartHeight) - evData.yDiff >= 0) ||
|
897 | (evData.yCrossOver && -Math.abs(evData.cropStartHeight) - evData.yDiff <= 0))
|
898 | ) {
|
899 | evData.yCrossOver = !evData.yCrossOver;
|
900 | }
|
901 | }
|
902 |
|
903 | render() {
|
904 | const {
|
905 | children,
|
906 | circularCrop,
|
907 | className,
|
908 | crossorigin,
|
909 | crop,
|
910 | disabled,
|
911 | imageStyle,
|
912 | locked,
|
913 | imageAlt,
|
914 | onImageError,
|
915 | renderComponent,
|
916 | scale = 1,
|
917 | src,
|
918 | style,
|
919 | rotate = 0,
|
920 | ruleOfThirds,
|
921 | } = this.props;
|
922 |
|
923 | const { cropIsActive, newCropIsBeingDrawn } = this.state;
|
924 | const cropSelection = isCropValid(crop) && this.componentRef ? this.createCropSelection() : null;
|
925 |
|
926 | const componentClasses = clsx('ReactCrop', className, {
|
927 | 'ReactCrop--active': cropIsActive,
|
928 | 'ReactCrop--disabled': disabled,
|
929 | 'ReactCrop--locked': locked,
|
930 | 'ReactCrop--new-crop': newCropIsBeingDrawn,
|
931 | 'ReactCrop--fixed-aspect': crop && crop.aspect,
|
932 | 'ReactCrop--circular-crop': crop && circularCrop,
|
933 | 'ReactCrop--rule-of-thirds': crop && ruleOfThirds,
|
934 | 'ReactCrop--invisible-crop': !this.dragStarted && crop && !crop.width && !crop.height,
|
935 | });
|
936 |
|
937 | return (
|
938 | <div
|
939 | ref={this.componentRef}
|
940 | className={componentClasses}
|
941 | style={style}
|
942 | onPointerDown={this.onComponentPointerDown}
|
943 | tabIndex={0}
|
944 | onKeyDown={this.onComponentKeyDown}
|
945 | onKeyUp={this.onComponentKeyUp}
|
946 | >
|
947 | <div ref={this.mediaWrapperRef} style={{ transform: `scale(${scale}) rotate(${rotate}deg)` }}>
|
948 | {renderComponent || (
|
949 | <img
|
950 | ref={this.imageRef}
|
951 | crossOrigin={crossorigin}
|
952 | className="ReactCrop__image"
|
953 | style={imageStyle}
|
954 | src={src}
|
955 | onLoad={this.onImageLoad}
|
956 | onError={onImageError}
|
957 | alt={imageAlt}
|
958 | />
|
959 | )}
|
960 | </div>
|
961 | {children}
|
962 | {cropSelection}
|
963 | </div>
|
964 | );
|
965 | }
|
966 | }
|
967 |
|
968 | export { ReactCrop as default, ReactCrop as Component, makeAspectCrop, containCrop };
|