react-easy-crop
Version:
A React component to crop images/videos with easy interactions
951 lines (941 loc) • 42.1 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tslib'), require('react'), require('normalize-wheel')) :
typeof define === 'function' && define.amd ? define(['exports', 'tslib', 'react', 'normalize-wheel'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactEasyCrop = {}, global.tslib, global.React, global.normalizeWheel));
})(this, (function (exports, tslib, React, normalizeWheel) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespace(React);
var normalizeWheel__default = /*#__PURE__*/_interopDefaultLegacy(normalizeWheel);
/**
* Compute the dimension of the crop area based on media size,
* aspect ratio and optionally rotation
*/
function getCropSize(mediaWidth, mediaHeight, containerWidth, containerHeight, aspect, rotation) {
if (rotation === void 0) {
rotation = 0;
}
var _a = rotateSize(mediaWidth, mediaHeight, rotation),
width = _a.width,
height = _a.height;
var fittingWidth = Math.min(width, containerWidth);
var fittingHeight = Math.min(height, containerHeight);
if (fittingWidth > fittingHeight * aspect) {
return {
width: fittingHeight * aspect,
height: fittingHeight
};
}
return {
width: fittingWidth,
height: fittingWidth / aspect
};
}
/**
* Compute media zoom.
* We fit the media into the container with "max-width: 100%; max-height: 100%;"
*/
function getMediaZoom(mediaSize) {
// Take the axis with more pixels to improve accuracy
return mediaSize.width > mediaSize.height ? mediaSize.width / mediaSize.naturalWidth : mediaSize.height / mediaSize.naturalHeight;
}
/**
* Ensure a new media position stays in the crop area.
*/
function restrictPosition(position, mediaSize, cropSize, zoom, rotation) {
if (rotation === void 0) {
rotation = 0;
}
var _a = rotateSize(mediaSize.width, mediaSize.height, rotation),
width = _a.width,
height = _a.height;
return {
x: restrictPositionCoord(position.x, width, cropSize.width, zoom),
y: restrictPositionCoord(position.y, height, cropSize.height, zoom)
};
}
function restrictPositionCoord(position, mediaSize, cropSize, zoom) {
var maxPosition = mediaSize * zoom / 2 - cropSize / 2;
return clamp(position, -maxPosition, maxPosition);
}
function getDistanceBetweenPoints(pointA, pointB) {
return Math.sqrt(Math.pow(pointA.y - pointB.y, 2) + Math.pow(pointA.x - pointB.x, 2));
}
function getRotationBetweenPoints(pointA, pointB) {
return Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI;
}
/**
* Compute the output cropped area of the media in percentages and pixels.
* x/y are the top-left coordinates on the src media
*/
function computeCroppedArea(crop, mediaSize, cropSize, aspect, zoom, rotation, restrictPosition) {
if (rotation === void 0) {
rotation = 0;
}
if (restrictPosition === void 0) {
restrictPosition = true;
}
// if the media is rotated by the user, we cannot limit the position anymore
// as it might need to be negative.
var limitAreaFn = restrictPosition ? limitArea : noOp;
var mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation);
var mediaNaturalBBoxSize = rotateSize(mediaSize.naturalWidth, mediaSize.naturalHeight, rotation);
// calculate the crop area in percentages
// in the rotated space
var croppedAreaPercentages = {
x: limitAreaFn(100, ((mediaBBoxSize.width - cropSize.width / zoom) / 2 - crop.x / zoom) / mediaBBoxSize.width * 100),
y: limitAreaFn(100, ((mediaBBoxSize.height - cropSize.height / zoom) / 2 - crop.y / zoom) / mediaBBoxSize.height * 100),
width: limitAreaFn(100, cropSize.width / mediaBBoxSize.width * 100 / zoom),
height: limitAreaFn(100, cropSize.height / mediaBBoxSize.height * 100 / zoom)
};
// we compute the pixels size naively
var widthInPixels = Math.round(limitAreaFn(mediaNaturalBBoxSize.width, croppedAreaPercentages.width * mediaNaturalBBoxSize.width / 100));
var heightInPixels = Math.round(limitAreaFn(mediaNaturalBBoxSize.height, croppedAreaPercentages.height * mediaNaturalBBoxSize.height / 100));
var isImgWiderThanHigh = mediaNaturalBBoxSize.width >= mediaNaturalBBoxSize.height * aspect;
// then we ensure the width and height exactly match the aspect (to avoid rounding approximations)
// if the media is wider than high, when zoom is 0, the crop height will be equals to image height
// thus we want to compute the width from the height and aspect for accuracy.
// Otherwise, we compute the height from width and aspect.
var sizePixels = isImgWiderThanHigh ? {
width: Math.round(heightInPixels * aspect),
height: heightInPixels
} : {
width: widthInPixels,
height: Math.round(widthInPixels / aspect)
};
var croppedAreaPixels = tslib.__assign(tslib.__assign({}, sizePixels), {
x: Math.round(limitAreaFn(mediaNaturalBBoxSize.width - sizePixels.width, croppedAreaPercentages.x * mediaNaturalBBoxSize.width / 100)),
y: Math.round(limitAreaFn(mediaNaturalBBoxSize.height - sizePixels.height, croppedAreaPercentages.y * mediaNaturalBBoxSize.height / 100))
});
return {
croppedAreaPercentages: croppedAreaPercentages,
croppedAreaPixels: croppedAreaPixels
};
}
/**
* Ensure the returned value is between 0 and max
*/
function limitArea(max, value) {
return Math.min(max, Math.max(0, value));
}
function noOp(_max, value) {
return value;
}
/**
* Compute crop and zoom from the croppedAreaPercentages.
*/
function getInitialCropFromCroppedAreaPercentages(croppedAreaPercentages, mediaSize, rotation, cropSize, minZoom, maxZoom) {
var mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation);
// This is the inverse process of computeCroppedArea
var zoom = clamp(cropSize.width / mediaBBoxSize.width * (100 / croppedAreaPercentages.width), minZoom, maxZoom);
var crop = {
x: zoom * mediaBBoxSize.width / 2 - cropSize.width / 2 - mediaBBoxSize.width * zoom * (croppedAreaPercentages.x / 100),
y: zoom * mediaBBoxSize.height / 2 - cropSize.height / 2 - mediaBBoxSize.height * zoom * (croppedAreaPercentages.y / 100)
};
return {
crop: crop,
zoom: zoom
};
}
/**
* Compute zoom from the croppedAreaPixels
*/
function getZoomFromCroppedAreaPixels(croppedAreaPixels, mediaSize, cropSize) {
var mediaZoom = getMediaZoom(mediaSize);
return cropSize.height > cropSize.width ? cropSize.height / (croppedAreaPixels.height * mediaZoom) : cropSize.width / (croppedAreaPixels.width * mediaZoom);
}
/**
* Compute crop and zoom from the croppedAreaPixels
*/
function getInitialCropFromCroppedAreaPixels(croppedAreaPixels, mediaSize, rotation, cropSize, minZoom, maxZoom) {
if (rotation === void 0) {
rotation = 0;
}
var mediaNaturalBBoxSize = rotateSize(mediaSize.naturalWidth, mediaSize.naturalHeight, rotation);
var zoom = clamp(getZoomFromCroppedAreaPixels(croppedAreaPixels, mediaSize, cropSize), minZoom, maxZoom);
var cropZoom = cropSize.height > cropSize.width ? cropSize.height / croppedAreaPixels.height : cropSize.width / croppedAreaPixels.width;
var crop = {
x: ((mediaNaturalBBoxSize.width - croppedAreaPixels.width) / 2 - croppedAreaPixels.x) * cropZoom,
y: ((mediaNaturalBBoxSize.height - croppedAreaPixels.height) / 2 - croppedAreaPixels.y) * cropZoom
};
return {
crop: crop,
zoom: zoom
};
}
/**
* Return the point that is the center of point a and b
*/
function getCenter(a, b) {
return {
x: (b.x + a.x) / 2,
y: (b.y + a.y) / 2
};
}
function getRadianAngle(degreeValue) {
return degreeValue * Math.PI / 180;
}
/**
* Returns the new bounding area of a rotated rectangle.
*/
function rotateSize(width, height, rotation) {
var rotRad = getRadianAngle(rotation);
return {
width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height)
};
}
/**
* Clamp value between min and max
*/
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
/**
* Combine multiple class names into a single string.
*/
function classNames() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return args.filter(function (value) {
if (typeof value === 'string' && value.length > 0) {
return true;
}
return false;
}).join(' ').trim();
}
var css_248z = ".reactEasyCrop_Container {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n overflow: hidden;\n user-select: none;\n touch-action: none;\n cursor: move;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.reactEasyCrop_Image,\n.reactEasyCrop_Video {\n will-change: transform; /* this improves performances and prevent painting issues on iOS Chrome */\n}\n\n.reactEasyCrop_Contain {\n max-width: 100%;\n max-height: 100%;\n margin: auto;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n}\n.reactEasyCrop_Cover_Horizontal {\n width: 100%;\n height: auto;\n}\n.reactEasyCrop_Cover_Vertical {\n width: auto;\n height: 100%;\n}\n\n.reactEasyCrop_CropArea {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n border: 1px solid rgba(255, 255, 255, 0.5);\n box-sizing: border-box;\n box-shadow: 0 0 0 9999em;\n color: rgba(0, 0, 0, 0.5);\n overflow: hidden;\n}\n\n.reactEasyCrop_CropAreaRound {\n border-radius: 50%;\n}\n\n.reactEasyCrop_CropAreaGrid::before {\n content: ' ';\n box-sizing: border-box;\n position: absolute;\n border: 1px solid rgba(255, 255, 255, 0.5);\n top: 0;\n bottom: 0;\n left: 33.33%;\n right: 33.33%;\n border-top: 0;\n border-bottom: 0;\n}\n\n.reactEasyCrop_CropAreaGrid::after {\n content: ' ';\n box-sizing: border-box;\n position: absolute;\n border: 1px solid rgba(255, 255, 255, 0.5);\n top: 33.33%;\n bottom: 33.33%;\n left: 0;\n right: 0;\n border-left: 0;\n border-right: 0;\n}\n";
var MIN_ZOOM = 1;
var MAX_ZOOM = 3;
var KEYBOARD_STEP = 1;
var Cropper = /** @class */function (_super) {
tslib.__extends(Cropper, _super);
function Cropper() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.cropperRef = React__namespace.createRef();
_this.imageRef = React__namespace.createRef();
_this.videoRef = React__namespace.createRef();
_this.containerPosition = {
x: 0,
y: 0
};
_this.containerRef = null;
_this.styleRef = null;
_this.containerRect = null;
_this.mediaSize = {
width: 0,
height: 0,
naturalWidth: 0,
naturalHeight: 0
};
_this.dragStartPosition = {
x: 0,
y: 0
};
_this.dragStartCrop = {
x: 0,
y: 0
};
_this.gestureZoomStart = 0;
_this.gestureRotationStart = 0;
_this.isTouching = false;
_this.lastPinchDistance = 0;
_this.lastPinchRotation = 0;
_this.rafDragTimeout = null;
_this.rafPinchTimeout = null;
_this.wheelTimer = null;
_this.currentDoc = typeof document !== 'undefined' ? document : null;
_this.currentWindow = typeof window !== 'undefined' ? window : null;
_this.resizeObserver = null;
_this.state = {
cropSize: null,
hasWheelJustStarted: false,
mediaObjectFit: undefined
};
_this.initResizeObserver = function () {
if (typeof window.ResizeObserver === 'undefined' || !_this.containerRef) {
return;
}
var isFirstResize = true;
_this.resizeObserver = new window.ResizeObserver(function (entries) {
if (isFirstResize) {
isFirstResize = false; // observe() is called on mount, we don't want to trigger a recompute on mount
return;
}
_this.computeSizes();
});
_this.resizeObserver.observe(_this.containerRef);
};
// this is to prevent Safari on iOS >= 10 to zoom the page
_this.preventZoomSafari = function (e) {
return e.preventDefault();
};
_this.cleanEvents = function () {
if (!_this.currentDoc) return;
_this.currentDoc.removeEventListener('mousemove', _this.onMouseMove);
_this.currentDoc.removeEventListener('mouseup', _this.onDragStopped);
_this.currentDoc.removeEventListener('touchmove', _this.onTouchMove);
_this.currentDoc.removeEventListener('touchend', _this.onDragStopped);
_this.currentDoc.removeEventListener('gesturemove', _this.onGestureMove);
_this.currentDoc.removeEventListener('gestureend', _this.onGestureEnd);
_this.currentDoc.removeEventListener('scroll', _this.onScroll);
};
_this.clearScrollEvent = function () {
if (_this.containerRef) _this.containerRef.removeEventListener('wheel', _this.onWheel);
if (_this.wheelTimer) {
clearTimeout(_this.wheelTimer);
}
};
_this.onMediaLoad = function () {
var cropSize = _this.computeSizes();
if (cropSize) {
_this.emitCropData();
_this.setInitialCrop(cropSize);
}
if (_this.props.onMediaLoaded) {
_this.props.onMediaLoaded(_this.mediaSize);
}
};
_this.setInitialCrop = function (cropSize) {
if (_this.props.initialCroppedAreaPercentages) {
var _a = getInitialCropFromCroppedAreaPercentages(_this.props.initialCroppedAreaPercentages, _this.mediaSize, _this.props.rotation, cropSize, _this.props.minZoom, _this.props.maxZoom),
crop = _a.crop,
zoom = _a.zoom;
_this.props.onCropChange(crop);
_this.props.onZoomChange && _this.props.onZoomChange(zoom);
} else if (_this.props.initialCroppedAreaPixels) {
var _b = getInitialCropFromCroppedAreaPixels(_this.props.initialCroppedAreaPixels, _this.mediaSize, _this.props.rotation, cropSize, _this.props.minZoom, _this.props.maxZoom),
crop = _b.crop,
zoom = _b.zoom;
_this.props.onCropChange(crop);
_this.props.onZoomChange && _this.props.onZoomChange(zoom);
}
};
_this.computeSizes = function () {
var _a, _b, _c, _d, _e, _f;
var mediaRef = _this.imageRef.current || _this.videoRef.current;
if (mediaRef && _this.containerRef) {
_this.containerRect = _this.containerRef.getBoundingClientRect();
_this.saveContainerPosition();
var containerAspect = _this.containerRect.width / _this.containerRect.height;
var naturalWidth = ((_a = _this.imageRef.current) === null || _a === void 0 ? void 0 : _a.naturalWidth) || ((_b = _this.videoRef.current) === null || _b === void 0 ? void 0 : _b.videoWidth) || 0;
var naturalHeight = ((_c = _this.imageRef.current) === null || _c === void 0 ? void 0 : _c.naturalHeight) || ((_d = _this.videoRef.current) === null || _d === void 0 ? void 0 : _d.videoHeight) || 0;
var isMediaScaledDown = mediaRef.offsetWidth < naturalWidth || mediaRef.offsetHeight < naturalHeight;
var mediaAspect = naturalWidth / naturalHeight;
// We do not rely on the offsetWidth/offsetHeight if the media is scaled down
// as the values they report are rounded. That will result in precision losses
// when calculating zoom. We use the fact that the media is positionned relative
// to the container. That allows us to use the container's dimensions
// and natural aspect ratio of the media to calculate accurate media size.
// However, for this to work, the container should not be rotated
var renderedMediaSize = void 0;
if (isMediaScaledDown) {
switch (_this.state.mediaObjectFit) {
default:
case 'contain':
renderedMediaSize = containerAspect > mediaAspect ? {
width: _this.containerRect.height * mediaAspect,
height: _this.containerRect.height
} : {
width: _this.containerRect.width,
height: _this.containerRect.width / mediaAspect
};
break;
case 'horizontal-cover':
renderedMediaSize = {
width: _this.containerRect.width,
height: _this.containerRect.width / mediaAspect
};
break;
case 'vertical-cover':
renderedMediaSize = {
width: _this.containerRect.height * mediaAspect,
height: _this.containerRect.height
};
break;
}
} else {
renderedMediaSize = {
width: mediaRef.offsetWidth,
height: mediaRef.offsetHeight
};
}
_this.mediaSize = tslib.__assign(tslib.__assign({}, renderedMediaSize), {
naturalWidth: naturalWidth,
naturalHeight: naturalHeight
});
// set media size in the parent
if (_this.props.setMediaSize) {
_this.props.setMediaSize(_this.mediaSize);
}
var cropSize = _this.props.cropSize ? _this.props.cropSize : getCropSize(_this.mediaSize.width, _this.mediaSize.height, _this.containerRect.width, _this.containerRect.height, _this.props.aspect, _this.props.rotation);
if (((_e = _this.state.cropSize) === null || _e === void 0 ? void 0 : _e.height) !== cropSize.height || ((_f = _this.state.cropSize) === null || _f === void 0 ? void 0 : _f.width) !== cropSize.width) {
_this.props.onCropSizeChange && _this.props.onCropSizeChange(cropSize);
}
_this.setState({
cropSize: cropSize
}, _this.recomputeCropPosition);
// pass crop size to parent
if (_this.props.setCropSize) {
_this.props.setCropSize(cropSize);
}
return cropSize;
}
};
_this.saveContainerPosition = function () {
if (_this.containerRef) {
var bounds = _this.containerRef.getBoundingClientRect();
_this.containerPosition = {
x: bounds.left,
y: bounds.top
};
}
};
_this.onMouseDown = function (e) {
if (!_this.currentDoc) return;
e.preventDefault();
_this.currentDoc.addEventListener('mousemove', _this.onMouseMove);
_this.currentDoc.addEventListener('mouseup', _this.onDragStopped);
_this.saveContainerPosition();
_this.onDragStart(Cropper.getMousePoint(e));
};
_this.onMouseMove = function (e) {
return _this.onDrag(Cropper.getMousePoint(e));
};
_this.onScroll = function (e) {
if (!_this.currentDoc) return;
e.preventDefault();
_this.saveContainerPosition();
};
_this.onTouchStart = function (e) {
if (!_this.currentDoc) return;
_this.isTouching = true;
if (_this.props.onTouchRequest && !_this.props.onTouchRequest(e)) {
return;
}
_this.currentDoc.addEventListener('touchmove', _this.onTouchMove, {
passive: false
}); // iOS 11 now defaults to passive: true
_this.currentDoc.addEventListener('touchend', _this.onDragStopped);
_this.saveContainerPosition();
if (e.touches.length === 2) {
_this.onPinchStart(e);
} else if (e.touches.length === 1) {
_this.onDragStart(Cropper.getTouchPoint(e.touches[0]));
}
};
_this.onTouchMove = function (e) {
// Prevent whole page from scrolling on iOS.
e.preventDefault();
if (e.touches.length === 2) {
_this.onPinchMove(e);
} else if (e.touches.length === 1) {
_this.onDrag(Cropper.getTouchPoint(e.touches[0]));
}
};
_this.onGestureStart = function (e) {
if (!_this.currentDoc) return;
e.preventDefault();
_this.currentDoc.addEventListener('gesturechange', _this.onGestureMove);
_this.currentDoc.addEventListener('gestureend', _this.onGestureEnd);
_this.gestureZoomStart = _this.props.zoom;
_this.gestureRotationStart = _this.props.rotation;
};
_this.onGestureMove = function (e) {
e.preventDefault();
if (_this.isTouching) {
// this is to avoid conflict between gesture and touch events
return;
}
var point = Cropper.getMousePoint(e);
var newZoom = _this.gestureZoomStart - 1 + e.scale;
_this.setNewZoom(newZoom, point, {
shouldUpdatePosition: true
});
if (_this.props.onRotationChange) {
var newRotation = _this.gestureRotationStart + e.rotation;
_this.props.onRotationChange(newRotation);
}
};
_this.onGestureEnd = function (e) {
_this.cleanEvents();
};
_this.onDragStart = function (_a) {
var _b, _c;
var x = _a.x,
y = _a.y;
_this.dragStartPosition = {
x: x,
y: y
};
_this.dragStartCrop = tslib.__assign({}, _this.props.crop);
(_c = (_b = _this.props).onInteractionStart) === null || _c === void 0 ? void 0 : _c.call(_b);
};
_this.onDrag = function (_a) {
var x = _a.x,
y = _a.y;
if (!_this.currentWindow) return;
if (_this.rafDragTimeout) _this.currentWindow.cancelAnimationFrame(_this.rafDragTimeout);
_this.rafDragTimeout = _this.currentWindow.requestAnimationFrame(function () {
if (!_this.state.cropSize) return;
if (x === undefined || y === undefined) return;
var offsetX = x - _this.dragStartPosition.x;
var offsetY = y - _this.dragStartPosition.y;
var requestedPosition = {
x: _this.dragStartCrop.x + offsetX,
y: _this.dragStartCrop.y + offsetY
};
var newPosition = _this.props.restrictPosition ? restrictPosition(requestedPosition, _this.mediaSize, _this.state.cropSize, _this.props.zoom, _this.props.rotation) : requestedPosition;
_this.props.onCropChange(newPosition);
});
};
_this.onDragStopped = function () {
var _a, _b;
_this.isTouching = false;
_this.cleanEvents();
_this.emitCropData();
(_b = (_a = _this.props).onInteractionEnd) === null || _b === void 0 ? void 0 : _b.call(_a);
};
_this.onWheel = function (e) {
if (!_this.currentWindow) return;
if (_this.props.onWheelRequest && !_this.props.onWheelRequest(e)) {
return;
}
e.preventDefault();
var point = Cropper.getMousePoint(e);
var pixelY = normalizeWheel__default["default"](e).pixelY;
var newZoom = _this.props.zoom - pixelY * _this.props.zoomSpeed / 200;
_this.setNewZoom(newZoom, point, {
shouldUpdatePosition: true
});
if (!_this.state.hasWheelJustStarted) {
_this.setState({
hasWheelJustStarted: true
}, function () {
var _a, _b;
return (_b = (_a = _this.props).onInteractionStart) === null || _b === void 0 ? void 0 : _b.call(_a);
});
}
if (_this.wheelTimer) {
clearTimeout(_this.wheelTimer);
}
_this.wheelTimer = _this.currentWindow.setTimeout(function () {
return _this.setState({
hasWheelJustStarted: false
}, function () {
var _a, _b;
return (_b = (_a = _this.props).onInteractionEnd) === null || _b === void 0 ? void 0 : _b.call(_a);
});
}, 250);
};
_this.getPointOnContainer = function (_a, containerTopLeft) {
var x = _a.x,
y = _a.y;
if (!_this.containerRect) {
throw new Error('The Cropper is not mounted');
}
return {
x: _this.containerRect.width / 2 - (x - containerTopLeft.x),
y: _this.containerRect.height / 2 - (y - containerTopLeft.y)
};
};
_this.getPointOnMedia = function (_a) {
var x = _a.x,
y = _a.y;
var _b = _this.props,
crop = _b.crop,
zoom = _b.zoom;
return {
x: (x + crop.x) / zoom,
y: (y + crop.y) / zoom
};
};
_this.setNewZoom = function (zoom, point, _a) {
var _b = _a === void 0 ? {} : _a,
_c = _b.shouldUpdatePosition,
shouldUpdatePosition = _c === void 0 ? true : _c;
if (!_this.state.cropSize || !_this.props.onZoomChange) return;
var newZoom = clamp(zoom, _this.props.minZoom, _this.props.maxZoom);
if (shouldUpdatePosition) {
var zoomPoint = _this.getPointOnContainer(point, _this.containerPosition);
var zoomTarget = _this.getPointOnMedia(zoomPoint);
var requestedPosition = {
x: zoomTarget.x * newZoom - zoomPoint.x,
y: zoomTarget.y * newZoom - zoomPoint.y
};
var newPosition = _this.props.restrictPosition ? restrictPosition(requestedPosition, _this.mediaSize, _this.state.cropSize, newZoom, _this.props.rotation) : requestedPosition;
_this.props.onCropChange(newPosition);
}
_this.props.onZoomChange(newZoom);
};
_this.getCropData = function () {
if (!_this.state.cropSize) {
return null;
}
// this is to ensure the crop is correctly restricted after a zoom back (https://github.com/ValentinH/react-easy-crop/issues/6)
var restrictedPosition = _this.props.restrictPosition ? restrictPosition(_this.props.crop, _this.mediaSize, _this.state.cropSize, _this.props.zoom, _this.props.rotation) : _this.props.crop;
return computeCroppedArea(restrictedPosition, _this.mediaSize, _this.state.cropSize, _this.getAspect(), _this.props.zoom, _this.props.rotation, _this.props.restrictPosition);
};
_this.emitCropData = function () {
var cropData = _this.getCropData();
if (!cropData) return;
var croppedAreaPercentages = cropData.croppedAreaPercentages,
croppedAreaPixels = cropData.croppedAreaPixels;
if (_this.props.onCropComplete) {
_this.props.onCropComplete(croppedAreaPercentages, croppedAreaPixels);
}
if (_this.props.onCropAreaChange) {
_this.props.onCropAreaChange(croppedAreaPercentages, croppedAreaPixels);
}
};
_this.emitCropAreaChange = function () {
var cropData = _this.getCropData();
if (!cropData) return;
var croppedAreaPercentages = cropData.croppedAreaPercentages,
croppedAreaPixels = cropData.croppedAreaPixels;
if (_this.props.onCropAreaChange) {
_this.props.onCropAreaChange(croppedAreaPercentages, croppedAreaPixels);
}
};
_this.recomputeCropPosition = function () {
if (!_this.state.cropSize) return;
var newPosition = _this.props.restrictPosition ? restrictPosition(_this.props.crop, _this.mediaSize, _this.state.cropSize, _this.props.zoom, _this.props.rotation) : _this.props.crop;
_this.props.onCropChange(newPosition);
_this.emitCropData();
};
_this.onKeyDown = function (event) {
var _a, _b;
var _c = _this.props,
crop = _c.crop,
onCropChange = _c.onCropChange,
keyboardStep = _c.keyboardStep,
zoom = _c.zoom,
rotation = _c.rotation;
var step = keyboardStep;
if (!_this.state.cropSize) return;
// if the shift key is pressed, reduce the step to allow finer control
if (event.shiftKey) {
step *= 0.2;
}
var newCrop = tslib.__assign({}, crop);
switch (event.key) {
case 'ArrowUp':
newCrop.y -= step;
event.preventDefault();
break;
case 'ArrowDown':
newCrop.y += step;
event.preventDefault();
break;
case 'ArrowLeft':
newCrop.x -= step;
event.preventDefault();
break;
case 'ArrowRight':
newCrop.x += step;
event.preventDefault();
break;
default:
return;
}
if (_this.props.restrictPosition) {
newCrop = restrictPosition(newCrop, _this.mediaSize, _this.state.cropSize, zoom, rotation);
}
if (!event.repeat) {
(_b = (_a = _this.props).onInteractionStart) === null || _b === void 0 ? void 0 : _b.call(_a);
}
onCropChange(newCrop);
};
_this.onKeyUp = function (event) {
var _a, _b;
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
event.preventDefault();
break;
default:
return;
}
_this.emitCropData();
(_b = (_a = _this.props).onInteractionEnd) === null || _b === void 0 ? void 0 : _b.call(_a);
};
return _this;
}
Cropper.prototype.componentDidMount = function () {
if (!this.currentDoc || !this.currentWindow) return;
if (this.containerRef) {
if (this.containerRef.ownerDocument) {
this.currentDoc = this.containerRef.ownerDocument;
}
if (this.currentDoc.defaultView) {
this.currentWindow = this.currentDoc.defaultView;
}
this.initResizeObserver();
// only add window resize listener if ResizeObserver is not supported. Otherwise, it would be redundant
if (typeof window.ResizeObserver === 'undefined') {
this.currentWindow.addEventListener('resize', this.computeSizes);
}
this.props.zoomWithScroll && this.containerRef.addEventListener('wheel', this.onWheel, {
passive: false
});
this.containerRef.addEventListener('gesturestart', this.onGestureStart);
}
this.currentDoc.addEventListener('scroll', this.onScroll);
if (!this.props.disableAutomaticStylesInjection) {
this.styleRef = this.currentDoc.createElement('style');
this.styleRef.setAttribute('type', 'text/css');
if (this.props.nonce) {
this.styleRef.setAttribute('nonce', this.props.nonce);
}
this.styleRef.innerHTML = css_248z;
this.currentDoc.head.appendChild(this.styleRef);
}
// when rendered via SSR, the image can already be loaded and its onLoad callback will never be called
if (this.imageRef.current && this.imageRef.current.complete) {
this.onMediaLoad();
}
// set image and video refs in the parent if the callbacks exist
if (this.props.setImageRef) {
this.props.setImageRef(this.imageRef);
}
if (this.props.setVideoRef) {
this.props.setVideoRef(this.videoRef);
}
if (this.props.setCropperRef) {
this.props.setCropperRef(this.cropperRef);
}
};
Cropper.prototype.componentWillUnmount = function () {
var _a, _b;
if (!this.currentDoc || !this.currentWindow) return;
if (typeof window.ResizeObserver === 'undefined') {
this.currentWindow.removeEventListener('resize', this.computeSizes);
}
(_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
if (this.containerRef) {
this.containerRef.removeEventListener('gesturestart', this.preventZoomSafari);
}
if (this.styleRef) {
(_b = this.styleRef.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(this.styleRef);
}
this.cleanEvents();
this.props.zoomWithScroll && this.clearScrollEvent();
};
Cropper.prototype.componentDidUpdate = function (prevProps) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
if (prevProps.rotation !== this.props.rotation) {
this.computeSizes();
this.recomputeCropPosition();
} else if (prevProps.aspect !== this.props.aspect) {
this.computeSizes();
} else if (prevProps.objectFit !== this.props.objectFit) {
this.computeSizes();
} else if (prevProps.zoom !== this.props.zoom) {
this.recomputeCropPosition();
} else if (((_a = prevProps.cropSize) === null || _a === void 0 ? void 0 : _a.height) !== ((_b = this.props.cropSize) === null || _b === void 0 ? void 0 : _b.height) || ((_c = prevProps.cropSize) === null || _c === void 0 ? void 0 : _c.width) !== ((_d = this.props.cropSize) === null || _d === void 0 ? void 0 : _d.width)) {
this.computeSizes();
} else if (((_e = prevProps.crop) === null || _e === void 0 ? void 0 : _e.x) !== ((_f = this.props.crop) === null || _f === void 0 ? void 0 : _f.x) || ((_g = prevProps.crop) === null || _g === void 0 ? void 0 : _g.y) !== ((_h = this.props.crop) === null || _h === void 0 ? void 0 : _h.y)) {
this.emitCropAreaChange();
}
if (prevProps.zoomWithScroll !== this.props.zoomWithScroll && this.containerRef) {
this.props.zoomWithScroll ? this.containerRef.addEventListener('wheel', this.onWheel, {
passive: false
}) : this.clearScrollEvent();
}
if (prevProps.video !== this.props.video) {
(_j = this.videoRef.current) === null || _j === void 0 ? void 0 : _j.load();
}
var objectFit = this.getObjectFit();
if (objectFit !== this.state.mediaObjectFit) {
this.setState({
mediaObjectFit: objectFit
}, this.computeSizes);
}
};
Cropper.prototype.getAspect = function () {
var _a = this.props,
cropSize = _a.cropSize,
aspect = _a.aspect;
if (cropSize) {
return cropSize.width / cropSize.height;
}
return aspect;
};
Cropper.prototype.getObjectFit = function () {
var _a, _b, _c, _d;
if (this.props.objectFit === 'cover') {
var mediaRef = this.imageRef.current || this.videoRef.current;
if (mediaRef && this.containerRef) {
this.containerRect = this.containerRef.getBoundingClientRect();
var containerAspect = this.containerRect.width / this.containerRect.height;
var naturalWidth = ((_a = this.imageRef.current) === null || _a === void 0 ? void 0 : _a.naturalWidth) || ((_b = this.videoRef.current) === null || _b === void 0 ? void 0 : _b.videoWidth) || 0;
var naturalHeight = ((_c = this.imageRef.current) === null || _c === void 0 ? void 0 : _c.naturalHeight) || ((_d = this.videoRef.current) === null || _d === void 0 ? void 0 : _d.videoHeight) || 0;
var mediaAspect = naturalWidth / naturalHeight;
return mediaAspect < containerAspect ? 'horizontal-cover' : 'vertical-cover';
}
return 'horizontal-cover';
}
return this.props.objectFit;
};
Cropper.prototype.onPinchStart = function (e) {
var pointA = Cropper.getTouchPoint(e.touches[0]);
var pointB = Cropper.getTouchPoint(e.touches[1]);
this.lastPinchDistance = getDistanceBetweenPoints(pointA, pointB);
this.lastPinchRotation = getRotationBetweenPoints(pointA, pointB);
this.onDragStart(getCenter(pointA, pointB));
};
Cropper.prototype.onPinchMove = function (e) {
var _this = this;
if (!this.currentDoc || !this.currentWindow) return;
var pointA = Cropper.getTouchPoint(e.touches[0]);
var pointB = Cropper.getTouchPoint(e.touches[1]);
var center = getCenter(pointA, pointB);
this.onDrag(center);
if (this.rafPinchTimeout) this.currentWindow.cancelAnimationFrame(this.rafPinchTimeout);
this.rafPinchTimeout = this.currentWindow.requestAnimationFrame(function () {
var distance = getDistanceBetweenPoints(pointA, pointB);
var newZoom = _this.props.zoom * (distance / _this.lastPinchDistance);
_this.setNewZoom(newZoom, center, {
shouldUpdatePosition: false
});
_this.lastPinchDistance = distance;
var rotation = getRotationBetweenPoints(pointA, pointB);
var newRotation = _this.props.rotation + (rotation - _this.lastPinchRotation);
_this.props.onRotationChange && _this.props.onRotationChange(newRotation);
_this.lastPinchRotation = rotation;
});
};
Cropper.prototype.render = function () {
var _this = this;
var _a;
var _b = this.props,
image = _b.image,
video = _b.video,
mediaProps = _b.mediaProps,
cropperProps = _b.cropperProps,
transform = _b.transform,
_c = _b.crop,
x = _c.x,
y = _c.y,
rotation = _b.rotation,
zoom = _b.zoom,
cropShape = _b.cropShape,
showGrid = _b.showGrid,
_d = _b.style,
containerStyle = _d.containerStyle,
cropAreaStyle = _d.cropAreaStyle,
mediaStyle = _d.mediaStyle,
_e = _b.classes,
containerClassName = _e.containerClassName,
cropAreaClassName = _e.cropAreaClassName,
mediaClassName = _e.mediaClassName;
var objectFit = (_a = this.state.mediaObjectFit) !== null && _a !== void 0 ? _a : this.getObjectFit();
return React__namespace.createElement("div", {
onMouseDown: this.onMouseDown,
onTouchStart: this.onTouchStart,
ref: function ref(el) {
return _this.containerRef = el;
},
"data-testid": "container",
style: containerStyle,
className: classNames('reactEasyCrop_Container', containerClassName)
}, image ? React__namespace.createElement("img", tslib.__assign({
alt: "",
className: classNames('reactEasyCrop_Image', objectFit === 'contain' && 'reactEasyCrop_Contain', objectFit === 'horizontal-cover' && 'reactEasyCrop_Cover_Horizontal', objectFit === 'vertical-cover' && 'reactEasyCrop_Cover_Vertical', mediaClassName)
}, mediaProps, {
src: image,
ref: this.imageRef,
style: tslib.__assign(tslib.__assign({}, mediaStyle), {
transform: transform || "translate(".concat(x, "px, ").concat(y, "px) rotate(").concat(rotation, "deg) scale(").concat(zoom, ")")
}),
onLoad: this.onMediaLoad
})) : video && React__namespace.createElement("video", tslib.__assign({
autoPlay: true,
playsInline: true,
loop: true,
muted: true,
className: classNames('reactEasyCrop_Video', objectFit === 'contain' && 'reactEasyCrop_Contain', objectFit === 'horizontal-cover' && 'reactEasyCrop_Cover_Horizontal', objectFit === 'vertical-cover' && 'reactEasyCrop_Cover_Vertical', mediaClassName)
}, mediaProps, {
ref: this.videoRef,
onLoadedMetadata: this.onMediaLoad,
style: tslib.__assign(tslib.__assign({}, mediaStyle), {
transform: transform || "translate(".concat(x, "px, ").concat(y, "px) rotate(").concat(rotation, "deg) scale(").concat(zoom, ")")
}),
controls: false
}), (Array.isArray(video) ? video : [{
src: video
}]).map(function (item) {
return React__namespace.createElement("source", tslib.__assign({
key: item.src
}, item));
})), this.state.cropSize && React__namespace.createElement("div", tslib.__assign({
ref: this.cropperRef,
style: tslib.__assign(tslib.__assign({}, cropAreaStyle), {
width: this.state.cropSize.width,
height: this.state.cropSize.height
}),
tabIndex: 0,
onKeyDown: this.onKeyDown,
onKeyUp: this.onKeyUp,
"data-testid": "cropper",
className: classNames('reactEasyCrop_CropArea', cropShape === 'round' && 'reactEasyCrop_CropAreaRound', showGrid && 'reactEasyCrop_CropAreaGrid', cropAreaClassName)
}, cropperProps)));
};
Cropper.defaultProps = {
zoom: 1,
rotation: 0,
aspect: 4 / 3,
maxZoom: MAX_ZOOM,
minZoom: MIN_ZOOM,
cropShape: 'rect',
objectFit: 'contain',
showGrid: true,
style: {},
classes: {},
mediaProps: {},
cropperProps: {},
zoomSpeed: 1,
restrictPosition: true,
zoomWithScroll: true,
keyboardStep: KEYBOARD_STEP
};
Cropper.getMousePoint = function (e) {
return {
x: Number(e.clientX),
y: Number(e.clientY)
};
};
Cropper.getTouchPoint = function (touch) {
return {
x: Number(touch.clientX),
y: Number(touch.clientY)
};
};
return Cropper;
}(React__namespace.Component);
exports["default"] = Cropper;
exports.getInitialCropFromCroppedAreaPercentages = getInitialCropFromCroppedAreaPercentages;
exports.getInitialCropFromCroppedAreaPixels = getInitialCropFromCroppedAreaPixels;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=react-easy-crop.js.map