Web: Components: Replaced package react-avatar-editor to with local code
This commit is contained in:
parent
4d3bc676ce
commit
0f704b7f5e
@ -107,8 +107,6 @@
|
||||
"punycode": "^2.1.1",
|
||||
"rc-tree": "^2.1.3",
|
||||
"react-autosize-textarea": "^7.0.0",
|
||||
"react-avatar-edit": "^0.8.3",
|
||||
"react-avatar-editor": "11.0.6",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dropzone": "^10.2.1",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Dropzone from "react-dropzone";
|
||||
import ReactAvatarEditor from "react-avatar-editor";
|
||||
import ReactAvatarEditor from "./react-avatar-editor";
|
||||
import PropTypes from "prop-types";
|
||||
import Avatar from "../../avatar/index";
|
||||
import accepts from "attr-accept";
|
||||
|
760
web/ASC.Web.Components/src/components/avatar-editor/sub-components/react-avatar-editor/index.js
vendored
Normal file
760
web/ASC.Web.Components/src/components/avatar-editor/sub-components/react-avatar-editor/index.js
vendored
Normal file
@ -0,0 +1,760 @@
|
||||
/* eslint-env browser, node */
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
import loadImageURL from "./utils/load-image-url";
|
||||
import loadImageFile from "./utils/load-image-file";
|
||||
|
||||
const makeCancelable = promise => {
|
||||
let hasCanceled_ = false;
|
||||
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
promise.then(
|
||||
val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
|
||||
error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
|
||||
);
|
||||
/* eslint-enable */
|
||||
});
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel() {
|
||||
hasCanceled_ = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const isTouchDevice = !!(
|
||||
typeof window !== "undefined" &&
|
||||
typeof navigator !== "undefined" &&
|
||||
("ontouchstart" in window || navigator.msMaxTouchPoints > 0)
|
||||
);
|
||||
|
||||
const isFileAPISupported = typeof File !== "undefined";
|
||||
|
||||
const isPassiveSupported = () => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
|
||||
let passiveSupported = false;
|
||||
try {
|
||||
const options = Object.defineProperty({}, "passive", {
|
||||
get: function() {
|
||||
passiveSupported = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("test", options, options);
|
||||
window.removeEventListener("test", options, options);
|
||||
} catch (err) {
|
||||
passiveSupported = false;
|
||||
}
|
||||
return passiveSupported;
|
||||
};
|
||||
|
||||
const draggableEvents = {
|
||||
touch: {
|
||||
react: {
|
||||
down: "onTouchStart",
|
||||
mouseDown: "onMouseDown",
|
||||
drag: "onTouchMove",
|
||||
move: "onTouchMove",
|
||||
mouseMove: "onMouseMove",
|
||||
up: "onTouchEnd",
|
||||
mouseUp: "onMouseUp"
|
||||
},
|
||||
native: {
|
||||
down: "touchstart",
|
||||
mouseDown: "mousedown",
|
||||
drag: "touchmove",
|
||||
move: "touchmove",
|
||||
mouseMove: "mousemove",
|
||||
up: "touchend",
|
||||
mouseUp: "mouseup"
|
||||
}
|
||||
},
|
||||
desktop: {
|
||||
react: {
|
||||
down: "onMouseDown",
|
||||
drag: "onDragOver",
|
||||
move: "onMouseMove",
|
||||
up: "onMouseUp"
|
||||
},
|
||||
native: {
|
||||
down: "mousedown",
|
||||
drag: "dragStart",
|
||||
move: "mousemove",
|
||||
up: "mouseup"
|
||||
}
|
||||
}
|
||||
};
|
||||
const deviceEvents = isTouchDevice
|
||||
? draggableEvents.touch
|
||||
: draggableEvents.desktop;
|
||||
|
||||
let pixelRatio =
|
||||
typeof window !== "undefined" && window.devicePixelRatio
|
||||
? window.devicePixelRatio
|
||||
: 1;
|
||||
|
||||
// Draws a rounded rectangle on a 2D context.
|
||||
const drawRoundedRect = (context, x, y, width, height, borderRadius) => {
|
||||
if (borderRadius === 0) {
|
||||
context.rect(x, y, width, height);
|
||||
} else {
|
||||
const widthMinusRad = width - borderRadius;
|
||||
const heightMinusRad = height - borderRadius;
|
||||
context.translate(x, y);
|
||||
context.arc(
|
||||
borderRadius,
|
||||
borderRadius,
|
||||
borderRadius,
|
||||
Math.PI,
|
||||
Math.PI * 1.5
|
||||
);
|
||||
context.lineTo(widthMinusRad, 0);
|
||||
context.arc(
|
||||
widthMinusRad,
|
||||
borderRadius,
|
||||
borderRadius,
|
||||
Math.PI * 1.5,
|
||||
Math.PI * 2
|
||||
);
|
||||
context.lineTo(width, heightMinusRad);
|
||||
context.arc(
|
||||
widthMinusRad,
|
||||
heightMinusRad,
|
||||
borderRadius,
|
||||
Math.PI * 2,
|
||||
Math.PI * 0.5
|
||||
);
|
||||
context.lineTo(borderRadius, height);
|
||||
context.arc(
|
||||
borderRadius,
|
||||
heightMinusRad,
|
||||
borderRadius,
|
||||
Math.PI * 0.5,
|
||||
Math.PI
|
||||
);
|
||||
context.translate(-x, -y);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultEmptyImage = {
|
||||
x: 0.5,
|
||||
y: 0.5
|
||||
};
|
||||
|
||||
class AvatarEditor extends React.Component {
|
||||
static propTypes = {
|
||||
scale: PropTypes.number,
|
||||
rotate: PropTypes.number,
|
||||
image: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
...(isFileAPISupported ? [PropTypes.instanceOf(File)] : [])
|
||||
]),
|
||||
border: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.arrayOf(PropTypes.number)
|
||||
]),
|
||||
borderRadius: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
position: PropTypes.shape({
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number
|
||||
}),
|
||||
color: PropTypes.arrayOf(PropTypes.number),
|
||||
crossOrigin: PropTypes.oneOf(["", "anonymous", "use-credentials"]),
|
||||
|
||||
onLoadFailure: PropTypes.func,
|
||||
onLoadSuccess: PropTypes.func,
|
||||
onImageReady: PropTypes.func,
|
||||
onImageChange: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseMove: PropTypes.func,
|
||||
onPositionChange: PropTypes.func,
|
||||
disableBoundaryChecks: PropTypes.bool,
|
||||
disableHiDPIScaling: PropTypes.bool,
|
||||
disableCanvasRotation: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
scale: 1,
|
||||
rotate: 0,
|
||||
border: 25,
|
||||
borderRadius: 0,
|
||||
width: 200,
|
||||
height: 200,
|
||||
color: [0, 0, 0, 0.5],
|
||||
onLoadFailure() {},
|
||||
onLoadSuccess() {},
|
||||
onImageReady() {},
|
||||
onImageChange() {},
|
||||
onMouseUp() {},
|
||||
onMouseMove() {},
|
||||
onPositionChange() {},
|
||||
disableBoundaryChecks: false,
|
||||
disableHiDPIScaling: false,
|
||||
disableCanvasRotation: true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
state = {
|
||||
drag: false,
|
||||
my: null,
|
||||
mx: null,
|
||||
image: defaultEmptyImage
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// scaling by the devicePixelRatio can impact performance on mobile as it creates a very large canvas. This is an override to increase performance.
|
||||
if (this.props.disableHiDPIScaling) {
|
||||
pixelRatio = 1;
|
||||
}
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (this.props.image) {
|
||||
this.loadImage(this.props.image);
|
||||
}
|
||||
this.paint(context);
|
||||
if (document) {
|
||||
const passiveSupported = isPassiveSupported();
|
||||
const thirdArgument = passiveSupported ? { passive: false } : false;
|
||||
|
||||
const nativeEvents = deviceEvents.native;
|
||||
document.addEventListener(
|
||||
nativeEvents.move,
|
||||
this.handleMouseMove,
|
||||
thirdArgument
|
||||
);
|
||||
document.addEventListener(
|
||||
nativeEvents.up,
|
||||
this.handleMouseUp,
|
||||
thirdArgument
|
||||
);
|
||||
if (isTouchDevice) {
|
||||
document.addEventListener(
|
||||
nativeEvents.mouseMove,
|
||||
this.handleMouseMove,
|
||||
thirdArgument
|
||||
);
|
||||
document.addEventListener(
|
||||
nativeEvents.mouseUp,
|
||||
this.handleMouseUp,
|
||||
thirdArgument
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
(this.props.image && this.props.image !== prevProps.image) ||
|
||||
this.props.width !== prevProps.width ||
|
||||
this.props.height !== prevProps.height
|
||||
) {
|
||||
this.loadImage(this.props.image);
|
||||
} else if (!this.props.image && prevState.image !== defaultEmptyImage) {
|
||||
this.clearImage();
|
||||
}
|
||||
|
||||
const context = this.canvas.getContext("2d");
|
||||
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.paint(context);
|
||||
this.paintImage(context, this.state.image, this.props.border);
|
||||
|
||||
if (
|
||||
prevProps.image !== this.props.image ||
|
||||
prevProps.width !== this.props.width ||
|
||||
prevProps.height !== this.props.height ||
|
||||
prevProps.position !== this.props.position ||
|
||||
prevProps.scale !== this.props.scale ||
|
||||
prevProps.rotate !== this.props.rotate ||
|
||||
prevState.my !== this.state.my ||
|
||||
prevState.mx !== this.state.mx ||
|
||||
prevState.image.x !== this.state.image.x ||
|
||||
prevState.image.y !== this.state.image.y
|
||||
) {
|
||||
this.props.onImageChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (document) {
|
||||
const nativeEvents = deviceEvents.native;
|
||||
document.removeEventListener(
|
||||
nativeEvents.move,
|
||||
this.handleMouseMove,
|
||||
false
|
||||
);
|
||||
document.removeEventListener(nativeEvents.up, this.handleMouseUp, false);
|
||||
if (isTouchDevice) {
|
||||
document.removeEventListener(
|
||||
nativeEvents.mouseMove,
|
||||
this.handleMouseMove,
|
||||
false
|
||||
);
|
||||
document.removeEventListener(
|
||||
nativeEvents.mouseUp,
|
||||
this.handleMouseUp,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isVertical() {
|
||||
return !this.props.disableCanvasRotation && this.props.rotate % 180 !== 0;
|
||||
}
|
||||
|
||||
getBorders(border = this.props.border) {
|
||||
return Array.isArray(border) ? border : [border, border];
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
const { width, height, rotate, border } = this.props;
|
||||
|
||||
const canvas = {};
|
||||
|
||||
const [borderX, borderY] = this.getBorders(border);
|
||||
|
||||
const canvasWidth = width;
|
||||
const canvasHeight = height;
|
||||
|
||||
if (this.isVertical()) {
|
||||
canvas.width = canvasHeight;
|
||||
canvas.height = canvasWidth;
|
||||
} else {
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
}
|
||||
|
||||
canvas.width += borderX * 2;
|
||||
canvas.height += borderY * 2;
|
||||
|
||||
return {
|
||||
canvas,
|
||||
rotate,
|
||||
width,
|
||||
height,
|
||||
border
|
||||
};
|
||||
}
|
||||
|
||||
getImage() {
|
||||
// get relative coordinates (0 to 1)
|
||||
const cropRect = this.getCroppingRect();
|
||||
const image = this.state.image;
|
||||
|
||||
// get actual pixel coordinates
|
||||
cropRect.x *= image.resource.width;
|
||||
cropRect.y *= image.resource.height;
|
||||
cropRect.width *= image.resource.width;
|
||||
cropRect.height *= image.resource.height;
|
||||
|
||||
// create a canvas with the correct dimensions
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
if (this.isVertical()) {
|
||||
canvas.width = cropRect.height;
|
||||
canvas.height = cropRect.width;
|
||||
} else {
|
||||
canvas.width = cropRect.width;
|
||||
canvas.height = cropRect.height;
|
||||
}
|
||||
|
||||
// draw the full-size image at the correct position,
|
||||
// the image gets truncated to the size of the canvas.
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
context.translate(canvas.width / 2, canvas.height / 2);
|
||||
context.rotate((this.props.rotate * Math.PI) / 180);
|
||||
context.translate(-(canvas.width / 2), -(canvas.height / 2));
|
||||
|
||||
if (this.isVertical()) {
|
||||
context.translate(
|
||||
(canvas.width - canvas.height) / 2,
|
||||
(canvas.height - canvas.width) / 2
|
||||
);
|
||||
}
|
||||
|
||||
context.drawImage(image.resource, -cropRect.x, -cropRect.y);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image scaled to original canvas size.
|
||||
* This was default in 4.x and is now kept as a legacy method.
|
||||
*/
|
||||
getImageScaledToCanvas() {
|
||||
const { width, height } = this.getDimensions();
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
if (this.isVertical()) {
|
||||
canvas.width = height;
|
||||
canvas.height = width;
|
||||
} else {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
// don't paint a border here, as it is the resulting image
|
||||
this.paintImage(canvas.getContext("2d"), this.state.image, 0, 1);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
getXScale() {
|
||||
const canvasAspect = this.props.width / this.props.height;
|
||||
const imageAspect = this.state.image.width / this.state.image.height;
|
||||
|
||||
return Math.min(1, canvasAspect / imageAspect);
|
||||
}
|
||||
|
||||
getYScale() {
|
||||
const canvasAspect = this.props.height / this.props.width;
|
||||
const imageAspect = this.state.image.height / this.state.image.width;
|
||||
|
||||
return Math.min(1, canvasAspect / imageAspect);
|
||||
}
|
||||
|
||||
getCroppingRect() {
|
||||
const position = this.props.position || {
|
||||
x: this.state.image.x,
|
||||
y: this.state.image.y
|
||||
};
|
||||
const width = (1 / this.props.scale) * this.getXScale();
|
||||
const height = (1 / this.props.scale) * this.getYScale();
|
||||
|
||||
const croppingRect = {
|
||||
x: position.x - width / 2,
|
||||
y: position.y - height / 2,
|
||||
width,
|
||||
height
|
||||
};
|
||||
|
||||
let xMin = 0;
|
||||
let xMax = 1 - croppingRect.width;
|
||||
let yMin = 0;
|
||||
let yMax = 1 - croppingRect.height;
|
||||
|
||||
// If the cropping rect is larger than the image, then we need to change
|
||||
// our maxima & minima for x & y to allow the image to appear anywhere up
|
||||
// to the very edge of the cropping rect.
|
||||
const isLargerThanImage =
|
||||
this.props.disableBoundaryChecks || width > 1 || height > 1;
|
||||
|
||||
if (isLargerThanImage) {
|
||||
xMin = -croppingRect.width;
|
||||
xMax = 1;
|
||||
yMin = -croppingRect.height;
|
||||
yMax = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...croppingRect,
|
||||
x: Math.max(xMin, Math.min(croppingRect.x, xMax)),
|
||||
y: Math.max(yMin, Math.min(croppingRect.y, yMax))
|
||||
};
|
||||
}
|
||||
|
||||
loadImage(image) {
|
||||
if (isFileAPISupported && image instanceof File) {
|
||||
this.loadingImage = makeCancelable(loadImageFile(image))
|
||||
.promise.then(this.handleImageReady)
|
||||
.catch(this.props.onLoadFailure);
|
||||
} else if (typeof image === "string") {
|
||||
this.loadingImage = makeCancelable(
|
||||
loadImageURL(image, this.props.crossOrigin)
|
||||
)
|
||||
.promise.then(this.handleImageReady)
|
||||
.catch(this.props.onLoadFailure);
|
||||
}
|
||||
}
|
||||
|
||||
handleImageReady = image => {
|
||||
const imageState = this.getInitialSize(image.width, image.height);
|
||||
imageState.resource = image;
|
||||
imageState.x = 0.5;
|
||||
imageState.y = 0.5;
|
||||
this.setState({ drag: false, image: imageState }, this.props.onImageReady);
|
||||
this.props.onLoadSuccess(imageState);
|
||||
};
|
||||
|
||||
getInitialSize(width, height) {
|
||||
let newHeight;
|
||||
let newWidth;
|
||||
|
||||
const dimensions = this.getDimensions();
|
||||
const canvasRatio = dimensions.height / dimensions.width;
|
||||
const imageRatio = height / width;
|
||||
|
||||
if (canvasRatio > imageRatio) {
|
||||
newHeight = this.getDimensions().height;
|
||||
newWidth = width * (newHeight / height);
|
||||
} else {
|
||||
newWidth = this.getDimensions().width;
|
||||
newHeight = height * (newWidth / width);
|
||||
}
|
||||
|
||||
return {
|
||||
height: newHeight,
|
||||
width: newWidth
|
||||
};
|
||||
}
|
||||
|
||||
clearImage = () => {
|
||||
const context = this.canvas.getContext("2d");
|
||||
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.setState({
|
||||
image: defaultEmptyImage
|
||||
});
|
||||
};
|
||||
|
||||
paintImage(context, image, border, scaleFactor = pixelRatio) {
|
||||
if (image.resource) {
|
||||
const position = this.calculatePosition(image, border);
|
||||
|
||||
context.save();
|
||||
|
||||
context.translate(context.canvas.width / 2, context.canvas.height / 2);
|
||||
context.rotate((this.props.rotate * Math.PI) / 180);
|
||||
context.translate(
|
||||
-(context.canvas.width / 2),
|
||||
-(context.canvas.height / 2)
|
||||
);
|
||||
|
||||
if (this.isVertical()) {
|
||||
context.translate(
|
||||
(context.canvas.width - context.canvas.height) / 2,
|
||||
(context.canvas.height - context.canvas.width) / 2
|
||||
);
|
||||
}
|
||||
|
||||
context.scale(scaleFactor, scaleFactor);
|
||||
|
||||
context.globalCompositeOperation = "destination-over";
|
||||
context.drawImage(
|
||||
image.resource,
|
||||
position.x,
|
||||
position.y,
|
||||
position.width,
|
||||
position.height
|
||||
);
|
||||
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
calculatePosition(image, border) {
|
||||
image = image || this.state.image;
|
||||
|
||||
const [borderX, borderY] = this.getBorders(border);
|
||||
|
||||
const croppingRect = this.getCroppingRect();
|
||||
|
||||
const width = image.width * this.props.scale;
|
||||
const height = image.height * this.props.scale;
|
||||
|
||||
let x = -croppingRect.x * width;
|
||||
let y = -croppingRect.y * height;
|
||||
|
||||
if (this.isVertical()) {
|
||||
x += borderY;
|
||||
y += borderX;
|
||||
} else {
|
||||
x += borderX;
|
||||
y += borderY;
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
height,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
paint(context) {
|
||||
context.save();
|
||||
context.scale(pixelRatio, pixelRatio);
|
||||
context.translate(0, 0);
|
||||
context.fillStyle = "rgba(" + this.props.color.slice(0, 4).join(",") + ")";
|
||||
|
||||
let borderRadius = this.props.borderRadius;
|
||||
const dimensions = this.getDimensions();
|
||||
const [borderSizeX, borderSizeY] = this.getBorders(dimensions.border);
|
||||
const height = dimensions.canvas.height;
|
||||
const width = dimensions.canvas.width;
|
||||
|
||||
// clamp border radius between zero (perfect rectangle) and half the size without borders (perfect circle or "pill")
|
||||
borderRadius = Math.max(borderRadius, 0);
|
||||
borderRadius = Math.min(
|
||||
borderRadius,
|
||||
width / 2 - borderSizeX,
|
||||
height / 2 - borderSizeY
|
||||
);
|
||||
|
||||
context.beginPath();
|
||||
// inner rect, possibly rounded
|
||||
drawRoundedRect(
|
||||
context,
|
||||
borderSizeX,
|
||||
borderSizeY,
|
||||
width - borderSizeX * 2,
|
||||
height - borderSizeY * 2,
|
||||
borderRadius
|
||||
);
|
||||
context.rect(width, 0, -width, height); // outer rect, drawn "counterclockwise"
|
||||
context.fill("evenodd");
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
handleMouseDown = e => {
|
||||
e = e || window.event;
|
||||
// if e is a touch event, preventDefault keeps
|
||||
// corresponding mouse events from also being fired
|
||||
// later.
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
drag: true,
|
||||
mx: null,
|
||||
my: null
|
||||
});
|
||||
};
|
||||
handleMouseUp = () => {
|
||||
if (this.state.drag) {
|
||||
this.setState({ drag: false });
|
||||
this.props.onMouseUp();
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseMove = e => {
|
||||
e = e || window.event;
|
||||
if (this.state.drag === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault(); // stop scrolling on iOS Safari
|
||||
|
||||
const mousePositionX = e.targetTouches
|
||||
? e.targetTouches[0].pageX
|
||||
: e.clientX;
|
||||
const mousePositionY = e.targetTouches
|
||||
? e.targetTouches[0].pageY
|
||||
: e.clientY;
|
||||
|
||||
const newState = {
|
||||
mx: mousePositionX,
|
||||
my: mousePositionY
|
||||
};
|
||||
|
||||
let rotate = this.props.rotate;
|
||||
|
||||
rotate %= 360;
|
||||
rotate = rotate < 0 ? rotate + 360 : rotate;
|
||||
|
||||
if (this.state.mx && this.state.my) {
|
||||
const mx = this.state.mx - mousePositionX;
|
||||
const my = this.state.my - mousePositionY;
|
||||
|
||||
const width = this.state.image.width * this.props.scale;
|
||||
const height = this.state.image.height * this.props.scale;
|
||||
|
||||
let { x: lastX, y: lastY } = this.getCroppingRect();
|
||||
|
||||
lastX *= width;
|
||||
lastY *= height;
|
||||
|
||||
// helpers to calculate vectors
|
||||
const toRadians = degree => degree * (Math.PI / 180);
|
||||
const cos = Math.cos(toRadians(rotate));
|
||||
const sin = Math.sin(toRadians(rotate));
|
||||
|
||||
const x = lastX + mx * cos + my * sin;
|
||||
const y = lastY + -mx * sin + my * cos;
|
||||
|
||||
const relativeWidth = (1 / this.props.scale) * this.getXScale();
|
||||
const relativeHeight = (1 / this.props.scale) * this.getYScale();
|
||||
|
||||
const position = {
|
||||
x: x / width + relativeWidth / 2,
|
||||
y: y / height + relativeHeight / 2
|
||||
};
|
||||
|
||||
this.props.onPositionChange(position);
|
||||
|
||||
newState.image = {
|
||||
...this.state.image,
|
||||
...position
|
||||
};
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
this.props.onMouseMove(e);
|
||||
};
|
||||
|
||||
setCanvas = canvas => {
|
||||
this.canvas = canvas;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
scale,
|
||||
rotate,
|
||||
image,
|
||||
border,
|
||||
borderRadius,
|
||||
width,
|
||||
height,
|
||||
position,
|
||||
color,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
style,
|
||||
crossOrigin,
|
||||
onLoadFailure,
|
||||
onLoadSuccess,
|
||||
onImageReady,
|
||||
onImageChange,
|
||||
onMouseUp,
|
||||
onMouseMove,
|
||||
onPositionChange,
|
||||
disableBoundaryChecks,
|
||||
disableHiDPIScaling,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const dimensions = this.getDimensions();
|
||||
const defaultStyle = {
|
||||
width: dimensions.canvas.width,
|
||||
height: dimensions.canvas.height,
|
||||
cursor: this.state.drag ? "grabbing" : "grab",
|
||||
touchAction: "none"
|
||||
};
|
||||
|
||||
const attributes = {
|
||||
width: dimensions.canvas.width * pixelRatio,
|
||||
height: dimensions.canvas.height * pixelRatio,
|
||||
style: {
|
||||
...defaultStyle,
|
||||
...style
|
||||
}
|
||||
};
|
||||
|
||||
attributes[deviceEvents.react.down] = this.handleMouseDown;
|
||||
if (isTouchDevice) {
|
||||
attributes[deviceEvents.react.mouseDown] = this.handleMouseDown;
|
||||
}
|
||||
|
||||
return <canvas ref={this.setCanvas} {...attributes} {...rest} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarEditor;
|
@ -0,0 +1,17 @@
|
||||
/* eslint-env browser, node */
|
||||
import loadImageURL from "./load-image-url";
|
||||
|
||||
export default function loadImageFile(imageFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
try {
|
||||
const image = loadImageURL(e.target.result);
|
||||
resolve(image);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
});
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/* eslint-env browser, node */
|
||||
|
||||
function isDataURL(str) {
|
||||
if (str === null) {
|
||||
return false;
|
||||
}
|
||||
const regex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[a-z0-9!$&',()*+;=\-._~:@/?%\s]*\s*$/i;
|
||||
return !!str.match(regex);
|
||||
}
|
||||
|
||||
export default function loadImageURL(imageURL, crossOrigin) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = reject;
|
||||
if (isDataURL(imageURL) === false && crossOrigin) {
|
||||
image.crossOrigin = crossOrigin;
|
||||
}
|
||||
image.src = imageURL;
|
||||
});
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* global DOMParser:false */
|
||||
|
||||
/*
|
||||
* This method uses DOMParser to parse an HTML string into
|
||||
* a document. By using this approach we avoid the potential
|
||||
* for XSS attacks on consumers of this component, which would
|
||||
* be created by parsing the string via a detached DOM fragment,
|
||||
* as in this latter case script in onload attributes will be
|
||||
* run in the context of the host page.
|
||||
*
|
||||
* The drawback to this approach is that browser support is not
|
||||
* as wide - IE10 and up along with evergreen browsers.
|
||||
*/
|
||||
const parseDOM = str => {
|
||||
const parser = typeof DOMParser === "undefined" ? null : new DOMParser();
|
||||
|
||||
if (!parser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parser.parseFromString(str, "text/html");
|
||||
};
|
||||
|
||||
export default parseDOM;
|
@ -0,0 +1,53 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import parseDOM from "./parse-dom";
|
||||
|
||||
describe("parseDOM", () => {
|
||||
let realDOMParser, result;
|
||||
|
||||
beforeEach(() => {
|
||||
realDOMParser = global.DOMParser;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.DOMParser = realDOMParser;
|
||||
});
|
||||
|
||||
describe("DOMParser available", () => {
|
||||
let parseFromString;
|
||||
|
||||
beforeEach(() => {
|
||||
parseFromString = jest.fn().mockReturnValue("%document%");
|
||||
global.DOMParser = jest.fn(() => ({ parseFromString }));
|
||||
|
||||
result = parseDOM('<div id="test"></div>');
|
||||
});
|
||||
|
||||
it("creates a new DOMParser", () => {
|
||||
expect(global.DOMParser).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls parseFromString with the passed-in string", () => {
|
||||
expect(parseFromString).toHaveBeenCalledWith(
|
||||
'<div id="test"></div>',
|
||||
"text/html"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the value returned by parseFromString", () => {
|
||||
expect(result).toBe("%document%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("No DOMParser available", () => {
|
||||
beforeEach(() => {
|
||||
global.DOMParser = undefined;
|
||||
|
||||
result = parseDOM('<div id="test"></div>');
|
||||
});
|
||||
|
||||
it("retuns null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,27 @@
|
||||
import parseDOM from "./parse-dom";
|
||||
|
||||
/*
|
||||
* Retrieves image URL from collection of data transfer
|
||||
* items, if one is present. As the item will contain
|
||||
* an HTML string containing an img element, it's
|
||||
* necessary to parse the HTML and then pull the src
|
||||
* attribute off the image.
|
||||
*/
|
||||
const retrieveImageURL = (dataTransferItems, callback) => {
|
||||
for (let i = 0; i < dataTransferItems.length; i++) {
|
||||
const item = dataTransferItems[i];
|
||||
|
||||
if (item.type === "text/html") {
|
||||
item.getAsString(value => {
|
||||
const doc = parseDOM(value);
|
||||
const img = doc.querySelector("img");
|
||||
if (img && img.src) {
|
||||
callback(img.src);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default retrieveImageURL;
|
@ -0,0 +1,105 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
describe("retrieveImageURL", () => {
|
||||
let retrieveImageURL, mockParseDOM, mockQuerySelector;
|
||||
|
||||
beforeEach(() => {
|
||||
mockQuerySelector = jest.fn();
|
||||
mockParseDOM = jest.fn();
|
||||
|
||||
jest.mock("./parse-dom", () => mockParseDOM);
|
||||
|
||||
retrieveImageURL = require("./retrieve-image-url").default;
|
||||
mockParseDOM = require("./parse-dom");
|
||||
});
|
||||
|
||||
it('runs getAsString on the first item with type "text/html"', () => {
|
||||
const items = [
|
||||
{ getAsString: jest.fn(), type: "something/else" },
|
||||
{ getAsString: jest.fn(), type: "text/html" }
|
||||
];
|
||||
|
||||
retrieveImageURL(items, () => {});
|
||||
|
||||
expect(items[0].getAsString).not.toHaveBeenCalled();
|
||||
expect(items[1].getAsString).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not run getAsString on later items with type "text/html"', () => {
|
||||
const items = [
|
||||
{ getAsString: jest.fn(), type: "text/html" },
|
||||
{ getAsString: jest.fn(), type: "text/html" }
|
||||
];
|
||||
|
||||
retrieveImageURL(items, () => {});
|
||||
|
||||
expect(items[0].getAsString).toHaveBeenCalled();
|
||||
expect(items[1].getAsString).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with html returned in getAsString callback", () => {
|
||||
let callback, invokeGetAsStringCallback;
|
||||
|
||||
beforeEach(() => {
|
||||
const items = [{ getAsString: jest.fn(), type: "text/html" }];
|
||||
|
||||
callback = jest.fn();
|
||||
|
||||
mockParseDOM.mockReturnValue({
|
||||
querySelector: mockQuerySelector
|
||||
});
|
||||
|
||||
retrieveImageURL(items, callback);
|
||||
|
||||
invokeGetAsStringCallback = () =>
|
||||
items[0].getAsString.mock.calls[0][0]('<div id="test-fragment"></div>');
|
||||
});
|
||||
|
||||
it("creates a document using parseDOM", () => {
|
||||
invokeGetAsStringCallback();
|
||||
expect(mockParseDOM).toHaveBeenCalledWith(
|
||||
'<div id="test-fragment"></div>'
|
||||
);
|
||||
});
|
||||
|
||||
it("searches for img elements", () => {
|
||||
invokeGetAsStringCallback();
|
||||
expect(mockQuerySelector).toHaveBeenCalledWith("img");
|
||||
});
|
||||
|
||||
describe("if the document contains an img with a src attribute", () => {
|
||||
beforeEach(() => {
|
||||
mockQuerySelector.mockReturnValue({
|
||||
src: "http://placekitten.com/100/100"
|
||||
});
|
||||
invokeGetAsStringCallback();
|
||||
});
|
||||
|
||||
it("should invoke the callback passed to retrieveImageURL with the value of the src attribute", () => {
|
||||
expect(callback).toHaveBeenCalledWith("http://placekitten.com/100/100");
|
||||
});
|
||||
});
|
||||
|
||||
describe("if the document contains an img without a src attribute", () => {
|
||||
beforeEach(() => {
|
||||
mockQuerySelector.mockReturnValue({});
|
||||
invokeGetAsStringCallback();
|
||||
});
|
||||
|
||||
it("should invoke the callback passed to retrieveImageURL with the value of the src attribute", () => {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("if the documetn does not contain an img", () => {
|
||||
beforeEach(() => {
|
||||
mockQuerySelector.mockReturnValue(null);
|
||||
invokeGetAsStringCallback();
|
||||
});
|
||||
|
||||
it("should invoke the callback passed to retrieveImageURL with the value of the src attribute", () => {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user