UNPKG

26.2 kBJavaScriptView Raw
1import React, { PureComponent } from 'react';
2import PropTypes from 'prop-types';
3import clsx from 'clsx';
4
5// Feature detection
6// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners
7let passiveSupported = false;
8
9try {
10 window.addEventListener(
11 'test',
12 null,
13 Object.defineProperty({}, 'passive', {
14 get: () => {
15 passiveSupported = true;
16 return true;
17 },
18 })
19 );
20} catch (err) {} // eslint-disable-line no-empty
21
22function getClientPos(e) {
23 let pageX;
24 let pageY;
25
26 if (e.touches) {
27 [{ pageX, pageY }] = e.touches;
28 } else {
29 ({ pageX, pageY } = e);
30 }
31
32 return {
33 x: pageX,
34 y: pageY,
35 };
36}
37
38function clamp(num, min, max) {
39 return Math.min(Math.max(num, min), max);
40}
41
42function isCropValid(crop) {
43 return crop && !isNaN(crop.width) && !isNaN(crop.height);
44}
45
46function inverseOrd(ord) {
47 if (ord === 'n') return 's';
48 if (ord === 'ne') return 'sw';
49 if (ord === 'e') return 'w';
50 if (ord === 'se') return 'nw';
51 if (ord === 's') return 'n';
52 if (ord === 'sw') return 'ne';
53 if (ord === 'w') return 'e';
54 if (ord === 'nw') return 'se';
55 return ord;
56}
57
58function makeAspectCrop(crop, imageWidth, imageHeight) {
59 if (isNaN(crop.aspect)) {
60 console.warn('`crop.aspect` should be a number in order to make an aspect crop', crop);
61 return crop;
62 }
63
64 const completeCrop = {
65 unit: 'px',
66 x: 0,
67 y: 0,
68 ...crop,
69 };
70
71 if (crop.width) {
72 completeCrop.height = completeCrop.width / crop.aspect;
73 }
74
75 if (crop.height) {
76 completeCrop.width = completeCrop.height * crop.aspect;
77 }
78
79 if (completeCrop.y + completeCrop.height > imageHeight) {
80 completeCrop.height = imageHeight - completeCrop.y;
81 completeCrop.width = completeCrop.height * crop.aspect;
82 }
83
84 if (completeCrop.x + completeCrop.width > imageWidth) {
85 completeCrop.width = imageWidth - completeCrop.x;
86 completeCrop.height = completeCrop.width / crop.aspect;
87 }
88
89 return completeCrop;
90}
91
92function convertToPercentCrop(crop, imageWidth, imageHeight) {
93 if (crop.unit === '%') {
94 return crop;
95 }
96
97 return {
98 unit: '%',
99 aspect: crop.aspect,
100 x: (crop.x / imageWidth) * 100,
101 y: (crop.y / imageHeight) * 100,
102 width: (crop.width / imageWidth) * 100,
103 height: (crop.height / imageHeight) * 100,
104 };
105}
106
107function convertToPixelCrop(crop, imageWidth, imageHeight) {
108 if (!crop.unit) {
109 return { ...crop, unit: 'px' };
110 }
111
112 if (crop.unit === 'px') {
113 return crop;
114 }
115
116 return {
117 unit: 'px',
118 aspect: crop.aspect,
119 x: (crop.x * imageWidth) / 100,
120 y: (crop.y * imageHeight) / 100,
121 width: (crop.width * imageWidth) / 100,
122 height: (crop.height * imageHeight) / 100,
123 };
124}
125
126function resolveCrop(pixelCrop, imageWidth, imageHeight) {
127 if (pixelCrop.aspect && (!pixelCrop.width || !pixelCrop.height)) {
128 return makeAspectCrop(pixelCrop, imageWidth, imageHeight);
129 }
130
131 return pixelCrop;
132}
133
134function containCrop(prevCrop, crop, imageWidth, imageHeight) {
135 const pixelCrop = convertToPixelCrop(crop, imageWidth, imageHeight);
136 const prevPixelCrop = convertToPixelCrop(prevCrop, imageWidth, imageHeight);
137 const contained = { ...pixelCrop };
138
139 // Non-aspects are simple
140 if (!pixelCrop.aspect) {
141 if (pixelCrop.x < 0) {
142 contained.x = 0;
143 contained.width += pixelCrop.x;
144 } else if (pixelCrop.x + pixelCrop.width > imageWidth) {
145 contained.width = imageWidth - pixelCrop.x;
146 }
147
148 if (pixelCrop.y + pixelCrop.height > imageHeight) {
149 contained.height = imageHeight - pixelCrop.y;
150 }
151
152 return contained;
153 }
154
155 let adjustedForX = false;
156
157 if (pixelCrop.x < 0) {
158 contained.x = 0;
159 contained.width += pixelCrop.x;
160 contained.height = contained.width / pixelCrop.aspect;
161 adjustedForX = true;
162 } else if (pixelCrop.x + pixelCrop.width > imageWidth) {
163 contained.width = imageWidth - pixelCrop.x;
164 contained.height = contained.width / pixelCrop.aspect;
165 adjustedForX = true;
166 }
167
168 // If sizing in up direction we need to pin Y at the point it
169 // would be at the boundary.
170 if (adjustedForX && prevPixelCrop.y > contained.y) {
171 contained.y = pixelCrop.y + (pixelCrop.height - contained.height);
172 }
173
174 let adjustedForY = false;
175
176 if (contained.y + contained.height > imageHeight) {
177 contained.height = imageHeight - pixelCrop.y;
178 contained.width = contained.height * pixelCrop.aspect;
179 adjustedForY = true;
180 }
181
182 // If sizing in left direction we need to pin X at the point it
183 // would be at the boundary.
184 if (adjustedForY && prevPixelCrop.x > contained.x) {
185 contained.x = pixelCrop.x + (pixelCrop.width - contained.width);
186 }
187
188 return contained;
189}
190
191class ReactCrop extends PureComponent {
192 window = typeof window !== 'undefined' ? window : {};
193
194 document = typeof document !== 'undefined' ? document : {};
195
196 state = {};
197
198 keysDown = new Set();
199
200 componentDidMount() {
201 if (this.document.addEventListener) {
202 const options = passiveSupported ? { passive: false } : false;
203
204 this.document.addEventListener('mousemove', this.onDocMouseTouchMove, options);
205 this.document.addEventListener('touchmove', this.onDocMouseTouchMove, options);
206
207 this.document.addEventListener('mouseup', this.onDocMouseTouchEnd, options);
208 this.document.addEventListener('touchend', this.onDocMouseTouchEnd, options);
209 this.document.addEventListener('touchcancel', this.onDocMouseTouchEnd, options);
210
211 this.componentRef.addEventListener('medialoaded', this.onMediaLoaded);
212 }
213 }
214
215 componentWillUnmount() {
216 if (this.document.removeEventListener) {
217 this.document.removeEventListener('mousemove', this.onDocMouseTouchMove);
218 this.document.removeEventListener('touchmove', this.onDocMouseTouchMove);
219
220 this.document.removeEventListener('mouseup', this.onDocMouseTouchEnd);
221 this.document.removeEventListener('touchend', this.onDocMouseTouchEnd);
222 this.document.removeEventListener('touchcancel', this.onDocMouseTouchEnd);
223
224 this.componentRef.removeEventListener('medialoaded', this.onMediaLoaded);
225 }
226 }
227
228 componentDidUpdate(prevProps) {
229 const { crop } = this.props;
230
231 if (
232 this.imageRef &&
233 prevProps.crop !== crop &&
234 crop.aspect &&
235 ((crop.width && !crop.height) || (!crop.width && crop.height))
236 ) {
237 const { width, height } = this.imageRef;
238 const newCrop = this.makeNewCrop();
239 const completedCrop = makeAspectCrop(newCrop, width, height);
240
241 const pixelCrop = convertToPixelCrop(completedCrop, width, height);
242 const percentCrop = convertToPercentCrop(completedCrop, width, height);
243 this.props.onChange(pixelCrop, percentCrop);
244 this.props.onComplete(pixelCrop, percentCrop);
245 }
246 }
247
248 onCropMouseTouchDown = e => {
249 const { crop, disabled } = this.props;
250 const { width, height } = this.mediaDimensions;
251 const pixelCrop = convertToPixelCrop(crop, width, height);
252
253 if (disabled) {
254 return;
255 }
256 e.preventDefault(); // Stop drag selection.
257
258 const clientPos = getClientPos(e);
259
260 // Focus for detecting keypress.
261 if (this.componentRef.setActive) {
262 this.componentRef.setActive({ preventScroll: true }); // IE/Edge #289
263 } else {
264 this.componentRef.focus({ preventScroll: true }); // All other browsers
265 }
266
267 const { ord } = e.target.dataset;
268 const xInversed = ord === 'nw' || ord === 'w' || ord === 'sw';
269 const yInversed = ord === 'nw' || ord === 'n' || ord === 'ne';
270
271 let cropOffset;
272
273 if (pixelCrop.aspect) {
274 cropOffset = this.getElementOffset(this.cropSelectRef);
275 }
276
277 this.evData = {
278 clientStartX: clientPos.x,
279 clientStartY: clientPos.y,
280 cropStartWidth: pixelCrop.width,
281 cropStartHeight: pixelCrop.height,
282 cropStartX: xInversed ? pixelCrop.x + pixelCrop.width : pixelCrop.x,
283 cropStartY: yInversed ? pixelCrop.y + pixelCrop.height : pixelCrop.y,
284 xInversed,
285 yInversed,
286 xCrossOver: xInversed,
287 yCrossOver: yInversed,
288 startXCrossOver: xInversed,
289 startYCrossOver: yInversed,
290 isResize: e.target.dataset.ord,
291 ord,
292 cropOffset,
293 };
294
295 this.mouseDownOnCrop = true;
296 this.setState({ cropIsActive: true });
297 };
298
299 onComponentMouseTouchDown = e => {
300 const { crop, disabled, locked, keepSelection, onChange } = this.props;
301
302 const componentEl = this.mediaWrapperRef.firstChild;
303
304 if (e.target !== componentEl || !componentEl.contains(e.target)) {
305 return;
306 }
307
308 if (disabled || locked || (keepSelection && isCropValid(crop))) {
309 return;
310 }
311
312 e.preventDefault(); // Stop drag selection.
313
314 const clientPos = getClientPos(e);
315
316 // Focus for detecting keypress.
317 if (this.componentRef.setActive) {
318 this.componentRef.setActive({ preventScroll: true }); // IE/Edge #289
319 } else {
320 this.componentRef.focus({ preventScroll: true }); // All other browsers
321 }
322
323 const mediaOffset = this.getElementOffset(this.mediaWrapperRef);
324 const x = clientPos.x - mediaOffset.left;
325 const y = clientPos.y - mediaOffset.top;
326
327 const nextCrop = {
328 unit: 'px',
329 aspect: crop ? crop.aspect : undefined,
330 x,
331 y,
332 width: 0,
333 height: 0,
334 };
335
336 this.evData = {
337 clientStartX: clientPos.x,
338 clientStartY: clientPos.y,
339 cropStartWidth: nextCrop.width,
340 cropStartHeight: nextCrop.height,
341 cropStartX: nextCrop.x,
342 cropStartY: nextCrop.y,
343 xInversed: false,
344 yInversed: false,
345 xCrossOver: false,
346 yCrossOver: false,
347 startXCrossOver: false,
348 startYCrossOver: false,
349 isResize: true,
350 ord: 'nw',
351 };
352
353 this.mouseDownOnCrop = true;
354
355 const { width, height } = this.mediaDimensions;
356
357 onChange(convertToPixelCrop(nextCrop, width, height), convertToPercentCrop(nextCrop, width, height));
358
359 this.setState({ cropIsActive: true, newCropIsBeingDrawn: true });
360 };
361
362 onDocMouseTouchMove = e => {
363 const { crop, disabled, onChange, onDragStart } = this.props;
364
365 if (disabled) {
366 return;
367 }
368
369 if (!this.mouseDownOnCrop) {
370 return;
371 }
372
373 e.preventDefault(); // Stop drag selection.
374
375 if (!this.dragStarted) {
376 this.dragStarted = true;
377 onDragStart(e);
378 }
379
380 const { evData } = this;
381 const clientPos = getClientPos(e);
382
383 if (evData.isResize && crop.aspect && evData.cropOffset) {
384 clientPos.y = this.straightenYPath(clientPos.x);
385 }
386
387 evData.xDiff = clientPos.x - evData.clientStartX;
388 evData.yDiff = clientPos.y - evData.clientStartY;
389
390 let nextCrop;
391
392 if (evData.isResize) {
393 nextCrop = this.resizeCrop();
394 } else {
395 nextCrop = this.dragCrop();
396 }
397
398 if (nextCrop !== crop) {
399 const { width, height } = this.mediaDimensions;
400 onChange(convertToPixelCrop(nextCrop, width, height), convertToPercentCrop(nextCrop, width, height));
401 }
402 };
403
404 onComponentKeyDown = e => {
405 const { crop, disabled, onChange, onComplete } = this.props;
406
407 if (disabled) {
408 return;
409 }
410
411 this.keysDown.add(e.key);
412 let nudged = false;
413
414 if (!isCropValid(crop)) {
415 return;
416 }
417
418 const nextCrop = this.makeNewCrop();
419 const ctrlCmdPressed = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey;
420 const nudgeStep = ctrlCmdPressed
421 ? ReactCrop.nudgeStepLarge
422 : e.shiftKey
423 ? ReactCrop.nudgeStepMedium
424 : ReactCrop.nudgeStep;
425
426 if (this.keysDown.has('ArrowLeft')) {
427 nextCrop.x -= nudgeStep;
428 nudged = true;
429 }
430
431 if (this.keysDown.has('ArrowRight')) {
432 nextCrop.x += nudgeStep;
433 nudged = true;
434 }
435
436 if (this.keysDown.has('ArrowUp')) {
437 nextCrop.y -= nudgeStep;
438 nudged = true;
439 }
440
441 if (this.keysDown.has('ArrowDown')) {
442 nextCrop.y += nudgeStep;
443 nudged = true;
444 }
445
446 if (nudged) {
447 e.preventDefault(); // Stop drag selection.
448 const { width, height } = this.mediaDimensions;
449
450 nextCrop.x = clamp(nextCrop.x, 0, width - nextCrop.width);
451 nextCrop.y = clamp(nextCrop.y, 0, height - nextCrop.height);
452
453 const pixelCrop = convertToPixelCrop(nextCrop, width, height);
454 const percentCrop = convertToPercentCrop(nextCrop, width, height);
455
456 onChange(pixelCrop, percentCrop);
457 onComplete(pixelCrop, percentCrop);
458 }
459 };
460
461 onComponentKeyUp = e => {
462 this.keysDown.delete(e.key);
463 };
464
465 onDocMouseTouchEnd = e => {
466 const { crop, disabled, onComplete, onDragEnd } = this.props;
467
468 if (disabled) {
469 return;
470 }
471
472 if (this.mouseDownOnCrop) {
473 this.mouseDownOnCrop = false;
474 this.dragStarted = false;
475
476 const { width, height } = this.mediaDimensions;
477
478 onDragEnd(e);
479 onComplete(convertToPixelCrop(crop, width, height), convertToPercentCrop(crop, width, height));
480
481 this.setState({ cropIsActive: false, newCropIsBeingDrawn: false });
482 }
483 };
484
485 // When the image is loaded or when a custom component via `renderComponent` prop fires
486 // a custom "medialoaded" event.
487 createNewCrop() {
488 const { width, height } = this.mediaDimensions;
489 const crop = this.makeNewCrop();
490 const resolvedCrop = resolveCrop(crop, width, height);
491 const pixelCrop = convertToPixelCrop(resolvedCrop, width, height);
492 const percentCrop = convertToPercentCrop(resolvedCrop, width, height);
493 return { pixelCrop, percentCrop };
494 }
495
496 // Custom components (using `renderComponent`) should fire a custom event
497 // called "medialoaded" when they are loaded.
498 onMediaLoaded = () => {
499 const { onComplete, onChange } = this.props;
500 const { pixelCrop, percentCrop } = this.createNewCrop();
501 onChange(pixelCrop, percentCrop);
502 onComplete(pixelCrop, percentCrop);
503 };
504
505 onImageLoad(image) {
506 const { onComplete, onChange, onImageLoaded } = this.props;
507
508 // Return false from onImageLoaded if you set the crop with setState in there as otherwise
509 // the subsequent onChange + onComplete will not have your updated crop.
510 const res = onImageLoaded(image);
511
512 if (res !== false) {
513 const { pixelCrop, percentCrop } = this.createNewCrop();
514 onChange(pixelCrop, percentCrop);
515 onComplete(pixelCrop, percentCrop);
516 }
517 }
518
519 get mediaDimensions() {
520 const { clientWidth, clientHeight } = this.mediaWrapperRef;
521 return { width: clientWidth, height: clientHeight };
522 }
523
524 getDocumentOffset() {
525 const { clientTop = 0, clientLeft = 0 } = this.document.documentElement || {};
526 return { clientTop, clientLeft };
527 }
528
529 getWindowOffset() {
530 const { pageYOffset = 0, pageXOffset = 0 } = this.window;
531 return { pageYOffset, pageXOffset };
532 }
533
534 getElementOffset(el) {
535 const rect = el.getBoundingClientRect();
536 const doc = this.getDocumentOffset();
537 const win = this.getWindowOffset();
538
539 const top = rect.top + win.pageYOffset - doc.clientTop;
540 const left = rect.left + win.pageXOffset - doc.clientLeft;
541
542 return { top, left };
543 }
544
545 getCropStyle() {
546 const crop = this.makeNewCrop(this.props.crop ? this.props.crop.unit : 'px');
547
548 return {
549 top: `${crop.y}${crop.unit}`,
550 left: `${crop.x}${crop.unit}`,
551 width: `${crop.width}${crop.unit}`,
552 height: `${crop.height}${crop.unit}`,
553 };
554 }
555
556 getNewSize() {
557 const { crop, minWidth, maxWidth, minHeight, maxHeight } = this.props;
558 const { evData } = this;
559 const { width, height } = this.mediaDimensions;
560
561 // New width.
562 let newWidth = evData.cropStartWidth + evData.xDiff;
563
564 if (evData.xCrossOver) {
565 newWidth = Math.abs(newWidth);
566 }
567
568 newWidth = clamp(newWidth, minWidth, maxWidth || width);
569
570 // New height.
571 let newHeight;
572
573 if (crop.aspect) {
574 newHeight = newWidth / crop.aspect;
575 } else {
576 newHeight = evData.cropStartHeight + evData.yDiff;
577 }
578
579 if (evData.yCrossOver) {
580 // Cap if polarity is inversed and the height fills the y space.
581 newHeight = Math.min(Math.abs(newHeight), evData.cropStartY);
582 }
583
584 newHeight = clamp(newHeight, minHeight, maxHeight || height);
585
586 if (crop.aspect) {
587 newWidth = clamp(newHeight * crop.aspect, 0, width);
588 }
589
590 return {
591 width: newWidth,
592 height: newHeight,
593 };
594 }
595
596 dragCrop() {
597 const nextCrop = this.makeNewCrop();
598 const { evData } = this;
599 const { width, height } = this.mediaDimensions;
600
601 nextCrop.x = clamp(evData.cropStartX + evData.xDiff, 0, width - nextCrop.width);
602 nextCrop.y = clamp(evData.cropStartY + evData.yDiff, 0, height - nextCrop.height);
603
604 return nextCrop;
605 }
606
607 resizeCrop() {
608 const { evData } = this;
609 const nextCrop = this.makeNewCrop();
610 const { ord } = evData;
611
612 // On the inverse change the diff so it's the same and
613 // the same algo applies.
614 if (evData.xInversed) {
615 evData.xDiff -= evData.cropStartWidth * 2;
616 evData.xDiffPc -= evData.cropStartWidth * 2;
617 }
618 if (evData.yInversed) {
619 evData.yDiff -= evData.cropStartHeight * 2;
620 evData.yDiffPc -= evData.cropStartHeight * 2;
621 }
622
623 // New size.
624 const newSize = this.getNewSize();
625
626 // Adjust x/y to give illusion of 'staticness' as width/height is increased
627 // when polarity is inversed.
628 let newX = evData.cropStartX;
629 let newY = evData.cropStartY;
630
631 if (evData.xCrossOver) {
632 newX = nextCrop.x + (nextCrop.width - newSize.width);
633 }
634
635 if (evData.yCrossOver) {
636 // This not only removes the little "shake" when inverting at a diagonal, but for some
637 // reason y was way off at fast speeds moving sw->ne with fixed aspect only, I couldn't
638 // figure out why.
639 if (evData.lastYCrossover === false) {
640 newY = nextCrop.y - newSize.height;
641 } else {
642 newY = nextCrop.y + (nextCrop.height - newSize.height);
643 }
644 }
645
646 const { width, height } = this.mediaDimensions;
647 const containedCrop = containCrop(
648 this.props.crop,
649 {
650 unit: nextCrop.unit,
651 x: newX,
652 y: newY,
653 width: newSize.width,
654 height: newSize.height,
655 aspect: nextCrop.aspect,
656 },
657 width,
658 height
659 );
660
661 // Apply x/y/width/height changes depending on ordinate (fixed aspect always applies both).
662 if (nextCrop.aspect || ReactCrop.xyOrds.indexOf(ord) > -1) {
663 nextCrop.x = containedCrop.x;
664 nextCrop.y = containedCrop.y;
665 nextCrop.width = containedCrop.width;
666 nextCrop.height = containedCrop.height;
667 } else if (ReactCrop.xOrds.indexOf(ord) > -1) {
668 nextCrop.x = containedCrop.x;
669 nextCrop.width = containedCrop.width;
670 } else if (ReactCrop.yOrds.indexOf(ord) > -1) {
671 nextCrop.y = containedCrop.y;
672 nextCrop.height = containedCrop.height;
673 }
674
675 evData.lastYCrossover = evData.yCrossOver;
676 this.crossOverCheck();
677
678 return nextCrop;
679 }
680
681 straightenYPath(clientX) {
682 const { evData } = this;
683 const { ord } = evData;
684 const { cropOffset, cropStartWidth, cropStartHeight } = evData;
685 let k;
686 let d;
687
688 if (ord === 'nw' || ord === 'se') {
689 k = cropStartHeight / cropStartWidth;
690 d = cropOffset.top - cropOffset.left * k;
691 } else {
692 k = -cropStartHeight / cropStartWidth;
693 d = cropOffset.top + (cropStartHeight - cropOffset.left * k);
694 }
695
696 return k * clientX + d;
697 }
698
699 createCropSelection() {
700 const { disabled, locked, renderSelectionAddon, ruleOfThirds, crop } = this.props;
701 const style = this.getCropStyle();
702
703 return (
704 <div
705 ref={r => (this.cropSelectRef = r)}
706 style={style}
707 className="ReactCrop__crop-selection"
708 onMouseDown={this.onCropMouseTouchDown}
709 onTouchStart={this.onCropMouseTouchDown}
710 >
711 {!disabled && !locked && (
712 <div className="ReactCrop__drag-elements">
713 <div className="ReactCrop__drag-bar ord-n" data-ord="n" />
714 <div className="ReactCrop__drag-bar ord-e" data-ord="e" />
715 <div className="ReactCrop__drag-bar ord-s" data-ord="s" />
716 <div className="ReactCrop__drag-bar ord-w" data-ord="w" />
717
718 <div className="ReactCrop__drag-handle ord-nw" data-ord="nw" />
719 <div className="ReactCrop__drag-handle ord-n" data-ord="n" />
720 <div className="ReactCrop__drag-handle ord-ne" data-ord="ne" />
721 <div className="ReactCrop__drag-handle ord-e" data-ord="e" />
722 <div className="ReactCrop__drag-handle ord-se" data-ord="se" />
723 <div className="ReactCrop__drag-handle ord-s" data-ord="s" />
724 <div className="ReactCrop__drag-handle ord-sw" data-ord="sw" />
725 <div className="ReactCrop__drag-handle ord-w" data-ord="w" />
726 </div>
727 )}
728 {renderSelectionAddon && isCropValid(crop) && (
729 <div className="ReactCrop__selection-addon" onMouseDown={e => e.stopPropagation()}>
730 {renderSelectionAddon(this.state)}
731 </div>
732 )}
733 {ruleOfThirds && (
734 <>
735 <div className="ReactCrop__rule-of-thirds-hz" />
736 <div className="ReactCrop__rule-of-thirds-vt" />
737 </>
738 )}
739 </div>
740 );
741 }
742
743 makeNewCrop(unit = 'px') {
744 const crop = { ...ReactCrop.defaultCrop, ...this.props.crop };
745 const { width, height } = this.mediaDimensions;
746
747 return unit === 'px' ? convertToPixelCrop(crop, width, height) : convertToPercentCrop(crop, width, height);
748 }
749
750 crossOverCheck() {
751 const { evData } = this;
752 const { minWidth, minHeight } = this.props;
753
754 if (
755 !minWidth &&
756 ((!evData.xCrossOver && -Math.abs(evData.cropStartWidth) - evData.xDiff >= 0) ||
757 (evData.xCrossOver && -Math.abs(evData.cropStartWidth) - evData.xDiff <= 0))
758 ) {
759 evData.xCrossOver = !evData.xCrossOver;
760 }
761
762 if (
763 !minHeight &&
764 ((!evData.yCrossOver && -Math.abs(evData.cropStartHeight) - evData.yDiff >= 0) ||
765 (evData.yCrossOver && -Math.abs(evData.cropStartHeight) - evData.yDiff <= 0))
766 ) {
767 evData.yCrossOver = !evData.yCrossOver;
768 }
769
770 const swapXOrd = evData.xCrossOver !== evData.startXCrossOver;
771 const swapYOrd = evData.yCrossOver !== evData.startYCrossOver;
772
773 evData.inversedXOrd = swapXOrd ? inverseOrd(evData.ord) : false;
774 evData.inversedYOrd = swapYOrd ? inverseOrd(evData.ord) : false;
775 }
776
777 render() {
778 const {
779 children,
780 circularCrop,
781 className,
782 crossorigin,
783 crop,
784 disabled,
785 locked,
786 imageAlt,
787 onImageError,
788 renderComponent,
789 src,
790 style,
791 imageStyle,
792 ruleOfThirds,
793 } = this.props;
794
795 const { cropIsActive, newCropIsBeingDrawn } = this.state;
796 const cropSelection = isCropValid(crop) && this.componentRef ? this.createCropSelection() : null;
797
798 const componentClasses = clsx('ReactCrop', className, {
799 'ReactCrop--active': cropIsActive,
800 'ReactCrop--disabled': disabled,
801 'ReactCrop--locked': locked,
802 'ReactCrop--new-crop': newCropIsBeingDrawn,
803 'ReactCrop--fixed-aspect': crop && crop.aspect,
804 'ReactCrop--circular-crop': crop && circularCrop,
805 'ReactCrop--rule-of-thirds': crop && ruleOfThirds,
806 'ReactCrop--invisible-crop': !this.dragStarted && crop && !crop.width && !crop.height,
807 });
808
809 return (
810 <div
811 ref={n => {
812 this.componentRef = n;
813 }}
814 className={componentClasses}
815 style={style}
816 onTouchStart={this.onComponentMouseTouchDown}
817 onMouseDown={this.onComponentMouseTouchDown}
818 tabIndex="0"
819 onKeyDown={this.onComponentKeyDown}
820 onKeyUp={this.onComponentKeyUp}
821 >
822 <div
823 ref={n => {
824 this.mediaWrapperRef = n;
825 }}
826 >
827 {renderComponent || (
828 <img
829 ref={r => (this.imageRef = r)}
830 crossOrigin={crossorigin}
831 className="ReactCrop__image"
832 style={imageStyle}
833 src={src}
834 onLoad={e => this.onImageLoad(e.target)}
835 onError={onImageError}
836 alt={imageAlt}
837 />
838 )}
839 </div>
840 {children}
841 {cropSelection}
842 </div>
843 );
844 }
845}
846
847ReactCrop.xOrds = ['e', 'w'];
848ReactCrop.yOrds = ['n', 's'];
849ReactCrop.xyOrds = ['nw', 'ne', 'se', 'sw'];
850
851ReactCrop.nudgeStep = 1;
852ReactCrop.nudgeStepMedium = 10;
853ReactCrop.nudgeStepLarge = 100;
854
855ReactCrop.defaultCrop = {
856 x: 0,
857 y: 0,
858 width: 0,
859 height: 0,
860 unit: 'px',
861};
862
863ReactCrop.propTypes = {
864 className: PropTypes.string,
865 children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
866 circularCrop: PropTypes.bool,
867 crop: PropTypes.shape({
868 aspect: PropTypes.number,
869 x: PropTypes.number,
870 y: PropTypes.number,
871 width: PropTypes.number,
872 height: PropTypes.number,
873 unit: PropTypes.oneOf(['px', '%']),
874 }),
875 crossorigin: PropTypes.string,
876 disabled: PropTypes.bool,
877 locked: PropTypes.bool,
878 imageAlt: PropTypes.string,
879 imageStyle: PropTypes.shape({}),
880 keepSelection: PropTypes.bool,
881 minWidth: PropTypes.number,
882 minHeight: PropTypes.number,
883 maxWidth: PropTypes.number,
884 maxHeight: PropTypes.number,
885 onChange: PropTypes.func.isRequired,
886 onImageError: PropTypes.func,
887 onComplete: PropTypes.func,
888 onImageLoaded: PropTypes.func,
889 onDragStart: PropTypes.func,
890 onDragEnd: PropTypes.func,
891 src: PropTypes.string.isRequired,
892 style: PropTypes.shape({}),
893 renderComponent: PropTypes.node,
894 renderSelectionAddon: PropTypes.func,
895 ruleOfThirds: PropTypes.bool,
896};
897
898ReactCrop.defaultProps = {
899 circularCrop: false,
900 className: undefined,
901 crop: undefined,
902 crossorigin: undefined,
903 disabled: false,
904 locked: false,
905 imageAlt: '',
906 maxWidth: undefined,
907 maxHeight: undefined,
908 minWidth: 0,
909 minHeight: 0,
910 keepSelection: false,
911 onComplete: () => {},
912 onImageError: () => {},
913 onImageLoaded: () => {},
914 onDragStart: () => {},
915 onDragEnd: () => {},
916 children: undefined,
917 style: undefined,
918 renderComponent: undefined,
919 imageStyle: undefined,
920 renderSelectionAddon: undefined,
921 ruleOfThirds: false,
922};
923
924export { ReactCrop as default, ReactCrop as Component, makeAspectCrop, containCrop };