import DEFAULTS from './defaults';
import TEMPLATE from './template';
import render from './render';
import preview from './preview';
import events from './events';
import handlers from './handlers';
import change from './change';
import methods from './methods';
import {
ACTION_ALL,
CLASS_HIDDEN,
CLASS_HIDE,
CLASS_INVISIBLE,
CLASS_MODAL,
CLASS_MOVE,
DATA_ACTION,
EVENT_CROP,
EVENT_ERROR,
EVENT_LOAD,
EVENT_READY,
NAMESPACE,
REGEXP_DATA_URL,
REGEXP_DATA_URL_JPEG,
REGEXP_TAG_NAME,
WINDOW,
} from './constants';
import {
addClass,
addListener,
addTimestamp,
arrayBufferToDataURL,
dataURLToArrayBuffer,
dispatchEvent,
extend,
getData,
getImageNaturalSizes,
getOrientation,
isCrossOriginURL,
isFunction,
isPlainObject,
parseOrientation,
proxy,
removeClass,
removeListener,
setData,
} from './utilities';
const AnotherCropper = WINDOW.Cropper;
class Cropper {
/**
* Create a new Cropper.
* @param {Element} element - The target element for cropping.
* @param {Object} [options={}] - The configuration options.
*/
constructor(element, options = {}) {
if (!element || !REGEXP_TAG_NAME.test(element.tagName)) {
throw new Error('The first argument is required and must be an <img> or <canvas> element.');
}
this.element = element;
this.options = extend({}, DEFAULTS, isPlainObject(options) && options);
this.complete = false;
this.cropped = false;
this.disabled = false;
this.isImg = false;
this.limited = false;
this.loaded = false;
this.ready = false;
this.replaced = false;
this.wheeling = false;
this.originalUrl = '';
this.canvasData = null;
this.cropBoxData = null;
this.previews = null;
this.pointers = {};
this.init();
}
init() {
const { element } = this;
const tagName = element.tagName.toLowerCase();
let url;
if (getData(element, NAMESPACE)) {
return;
}
setData(element, NAMESPACE, this);
if (tagName === 'img') {
this.isImg = true;
// e.g.: "img/picture.jpg"
url = element.getAttribute('src') || '';
this.originalUrl = url;
// Stop when it's a blank image
if (!url) {
return;
}
// e.g.: "http://example.com/img/picture.jpg"
url = element.src;
} else if (tagName === 'canvas' && window.HTMLCanvasElement) {
url = element.toDataURL();
}
this.load(url);
}
load(url) {
if (!url) {
return;
}
this.url = url;
this.imageData = {};
const { element, options } = this;
if (!options.checkOrientation || !window.ArrayBuffer) {
this.clone();
return;
}
// XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari
if (REGEXP_DATA_URL.test(url)) {
if (REGEXP_DATA_URL_JPEG.test(url)) {
this.read(dataURLToArrayBuffer(url));
} else {
this.clone();
}
return;
}
const xhr = new XMLHttpRequest();
xhr.onerror = () => {
this.clone();
};
xhr.onload = () => {
this.read(xhr.response);
};
// Bust cache when there is a "crossOrigin" property
if (options.checkCrossOrigin && isCrossOriginURL(url) && element.crossOrigin) {
url = addTimestamp(url);
}
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.withCredentials = element.crossOrigin === 'use-credentials';
xhr.send();
}
read(arrayBuffer) {
const { options, imageData } = this;
const orientation = getOrientation(arrayBuffer);
let rotate = 0;
let scaleX = 1;
let scaleY = 1;
if (orientation > 1) {
this.url = arrayBufferToDataURL(arrayBuffer, 'image/jpeg');
({ rotate, scaleX, scaleY } = parseOrientation(orientation));
}
if (options.rotatable) {
imageData.rotate = rotate;
}
if (options.scalable) {
imageData.scaleX = scaleX;
imageData.scaleY = scaleY;
}
this.clone();
}
clone() {
const { element, url } = this;
let crossOrigin;
let crossOriginUrl;
if (this.options.checkCrossOrigin && isCrossOriginURL(url)) {
({ crossOrigin } = element);
if (crossOrigin) {
crossOriginUrl = url;
} else {
crossOrigin = 'anonymous';
// Bust cache when there is not a "crossOrigin" property
crossOriginUrl = addTimestamp(url);
}
}
this.crossOrigin = crossOrigin;
this.crossOriginUrl = crossOriginUrl;
const image = document.createElement('img');
if (crossOrigin) {
image.crossOrigin = crossOrigin;
}
image.src = crossOriginUrl || url;
const start = proxy(this.start, this);
const stop = proxy(this.stop, this);
this.image = image;
this.onStart = start;
this.onStop = stop;
if (this.isImg) {
if (element.complete) {
this.start();
} else {
addListener(element, EVENT_LOAD, start);
}
} else {
addListener(image, EVENT_LOAD, start);
addListener(image, EVENT_ERROR, stop);
addClass(image, CLASS_HIDE);
element.parentNode.insertBefore(image, element.nextSibling);
}
}
start(event) {
const image = this.isImg ? this.element : this.image;
if (event) {
removeListener(image, EVENT_LOAD, this.onStart);
removeListener(image, EVENT_ERROR, this.onStop);
}
getImageNaturalSizes(image, (naturalWidth, naturalHeight) => {
extend(this.imageData, {
naturalWidth,
naturalHeight,
aspectRatio: naturalWidth / naturalHeight,
});
this.loaded = true;
this.build();
});
}
stop() {
const { image } = this;
removeListener(image, EVENT_LOAD, this.onStart);
removeListener(image, EVENT_ERROR, this.onStop);
image.parentNode.removeChild(image);
this.image = null;
}
build() {
if (!this.loaded) {
return;
}
// Unbuild first when replace
if (this.ready) {
this.unbuild();
}
const { element, options, image } = this;
// Create cropper elements
const container = element.parentNode;
const template = document.createElement('div');
template.innerHTML = TEMPLATE;
const cropper = template.querySelector(`.${NAMESPACE}-container`);
const canvas = cropper.querySelector(`.${NAMESPACE}-canvas`);
const dragBox = cropper.querySelector(`.${NAMESPACE}-drag-box`);
const cropBox = cropper.querySelector(`.${NAMESPACE}-crop-box`);
const face = cropBox.querySelector(`.${NAMESPACE}-face`);
this.container = container;
this.cropper = cropper;
this.canvas = canvas;
this.dragBox = dragBox;
this.cropBox = cropBox;
this.viewBox = cropper.querySelector(`.${NAMESPACE}-view-box`);
this.face = face;
canvas.appendChild(image);
// Hide the original image
addClass(element, CLASS_HIDDEN);
// Inserts the cropper after to the current image
container.insertBefore(cropper, element.nextSibling);
// Show the image if is hidden
if (!this.isImg) {
removeClass(image, CLASS_HIDE);
}
this.initPreview();
this.bind();
options.aspectRatio = Math.max(0, options.aspectRatio) || NaN;
options.viewMode = Math.max(0, Math.min(3, Math.round(options.viewMode))) || 0;
this.cropped = options.autoCrop;
if (options.autoCrop) {
if (options.modal) {
addClass(dragBox, CLASS_MODAL);
}
} else {
addClass(cropBox, CLASS_HIDDEN);
}
if (!options.guides) {
addClass(cropBox.getElementsByClassName(`${NAMESPACE}-dashed`), CLASS_HIDDEN);
}
if (!options.center) {
addClass(cropBox.getElementsByClassName(`${NAMESPACE}-center`), CLASS_HIDDEN);
}
if (options.background) {
addClass(cropper, `${NAMESPACE}-bg`);
}
if (!options.highlight) {
addClass(face, CLASS_INVISIBLE);
}
if (options.cropBoxMovable) {
addClass(face, CLASS_MOVE);
setData(face, DATA_ACTION, ACTION_ALL);
}
if (!options.cropBoxResizable) {
addClass(cropBox.getElementsByClassName(`${NAMESPACE}-line`), CLASS_HIDDEN);
addClass(cropBox.getElementsByClassName(`${NAMESPACE}-point`), CLASS_HIDDEN);
}
this.setDragMode(options.dragMode);
this.render();
this.ready = true;
this.setData(options.data);
// Call the "ready" option asynchronously to keep "image.cropper" is defined
this.completing = setTimeout(() => {
if (isFunction(options.ready)) {
addListener(element, EVENT_READY, options.ready, {
once: true,
});
}
dispatchEvent(element, EVENT_READY);
dispatchEvent(element, EVENT_CROP, this.getData());
this.complete = true;
}, 0);
}
unbuild() {
if (!this.ready) {
return;
}
if (!this.complete) {
clearTimeout(this.completing);
}
this.ready = false;
this.complete = false;
this.initialImageData = null;
// Clear `initialCanvasData` is necessary when replace
this.initialCanvasData = null;
this.initialCropBoxData = null;
this.containerData = null;
this.canvasData = null;
// Clear `cropBoxData` is necessary when replace
this.cropBoxData = null;
this.unbind();
this.resetPreview();
this.previews = null;
this.viewBox = null;
this.cropBox = null;
this.dragBox = null;
this.canvas = null;
this.container = null;
this.cropper.parentNode.removeChild(this.cropper);
this.cropper = null;
}
/**
* Get the no conflict cropper class.
* @returns {Cropper} The cropper class.
*/
static noConflict() {
window.Cropper = AnotherCropper;
return Cropper;
}
/**
* Change the default options.
* @param {Object} options - The new default options.
*/
static setDefaults(options) {
extend(DEFAULTS, isPlainObject(options) && options);
}
}
extend(Cropper.prototype, render, preview, events, handlers, change, methods);
export default Cropper;