1 | import React, { PureComponent } from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import clsx from 'clsx';
|
4 |
|
5 |
|
6 |
|
7 | let passiveSupported = false;
|
8 |
|
9 | try {
|
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) {}
|
21 |
|
22 | function 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 |
|
38 | function clamp(num, min, max) {
|
39 | return Math.min(Math.max(num, min), max);
|
40 | }
|
41 |
|
42 | function isCropValid(crop) {
|
43 | return crop && !isNaN(crop.width) && !isNaN(crop.height);
|
44 | }
|
45 |
|
46 | function 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 |
|
58 | function 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 |
|
92 | function 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 |
|
107 | function 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 |
|
126 | function 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 |
|
134 | function 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 |
|
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 |
|
169 |
|
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 |
|
183 |
|
184 | if (adjustedForY && prevPixelCrop.x > contained.x) {
|
185 | contained.x = pixelCrop.x + (pixelCrop.width - contained.width);
|
186 | }
|
187 |
|
188 | return contained;
|
189 | }
|
190 |
|
191 | class 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();
|
257 |
|
258 | const clientPos = getClientPos(e);
|
259 |
|
260 |
|
261 | if (this.componentRef.setActive) {
|
262 | this.componentRef.setActive({ preventScroll: true });
|
263 | } else {
|
264 | this.componentRef.focus({ preventScroll: true });
|
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();
|
313 |
|
314 | const clientPos = getClientPos(e);
|
315 |
|
316 |
|
317 | if (this.componentRef.setActive) {
|
318 | this.componentRef.setActive({ preventScroll: true });
|
319 | } else {
|
320 | this.componentRef.focus({ preventScroll: true });
|
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();
|
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();
|
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 |
|
486 |
|
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 |
|
497 |
|
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 |
|
509 |
|
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 |
|
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 |
|
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 |
|
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 |
|
613 |
|
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 |
|
624 | const newSize = this.getNewSize();
|
625 |
|
626 |
|
627 |
|
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 |
|
637 |
|
638 |
|
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 |
|
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 |
|
847 | ReactCrop.xOrds = ['e', 'w'];
|
848 | ReactCrop.yOrds = ['n', 's'];
|
849 | ReactCrop.xyOrds = ['nw', 'ne', 'se', 'sw'];
|
850 |
|
851 | ReactCrop.nudgeStep = 1;
|
852 | ReactCrop.nudgeStepMedium = 10;
|
853 | ReactCrop.nudgeStepLarge = 100;
|
854 |
|
855 | ReactCrop.defaultCrop = {
|
856 | x: 0,
|
857 | y: 0,
|
858 | width: 0,
|
859 | height: 0,
|
860 | unit: 'px',
|
861 | };
|
862 |
|
863 | ReactCrop.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 |
|
898 | ReactCrop.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 |
|
924 | export { ReactCrop as default, ReactCrop as Component, makeAspectCrop, containCrop };
|