aboutsummaryrefslogtreecommitdiffstats
path: root/library/cropperjs/src/js/cropper.js
diff options
context:
space:
mode:
Diffstat (limited to 'library/cropperjs/src/js/cropper.js')
-rw-r--r--library/cropperjs/src/js/cropper.js277
1 files changed, 152 insertions, 125 deletions
diff --git a/library/cropperjs/src/js/cropper.js b/library/cropperjs/src/js/cropper.js
index 97f3511df..a0cb85f3f 100644
--- a/library/cropperjs/src/js/cropper.js
+++ b/library/cropperjs/src/js/cropper.js
@@ -11,13 +11,10 @@ import {
CLASS_HIDDEN,
CLASS_HIDE,
CLASS_INVISIBLE,
- CLASS_MODAL,
CLASS_MOVE,
DATA_ACTION,
- EVENT_CROP,
- EVENT_ERROR,
- EVENT_LOAD,
EVENT_READY,
+ MIME_TYPE_JPEG,
NAMESPACE,
REGEXP_DATA_URL,
REGEXP_DATA_URL_JPEG,
@@ -29,19 +26,15 @@ import {
addListener,
addTimestamp,
arrayBufferToDataURL,
+ assign,
dataURLToArrayBuffer,
dispatchEvent,
- extend,
- getData,
- getImageNaturalSizes,
- getOrientation,
isCrossOriginURL,
isFunction,
isPlainObject,
parseOrientation,
- proxy,
removeClass,
- removeListener,
+ resetAndGetOrientation,
setData,
} from './utilities';
@@ -59,21 +52,15 @@ class Cropper {
}
this.element = element;
- this.options = extend({}, DEFAULTS, isPlainObject(options) && options);
- this.complete = false;
+ this.options = assign({}, DEFAULTS, isPlainObject(options) && options);
this.cropped = false;
this.disabled = false;
- this.isImg = false;
- this.limited = false;
- this.loaded = false;
+ this.pointers = {};
this.ready = false;
+ this.reloading = false;
this.replaced = false;
- this.wheeling = false;
- this.originalUrl = '';
- this.canvasData = null;
- this.cropBoxData = null;
- this.previews = null;
- this.pointers = {};
+ this.sized = false;
+ this.sizing = false;
this.init();
}
@@ -82,11 +69,11 @@ class Cropper {
const tagName = element.tagName.toLowerCase();
let url;
- if (getData(element, NAMESPACE)) {
+ if (element[NAMESPACE]) {
return;
}
- setData(element, NAMESPACE, this);
+ element[NAMESPACE] = this;
if (tagName === 'img') {
this.isImg = true;
@@ -100,7 +87,7 @@ class Cropper {
return;
}
- // e.g.: "http://example.com/img/picture.jpg"
+ // e.g.: "https://example.com/img/picture.jpg"
url = element.src;
} else if (tagName === 'canvas' && window.HTMLCanvasElement) {
url = element.toDataURL();
@@ -119,38 +106,68 @@ class Cropper {
const { element, options } = this;
+ if (!options.rotatable && !options.scalable) {
+ options.checkOrientation = false;
+ }
+
+ // Only IE10+ supports Typed Arrays
if (!options.checkOrientation || !window.ArrayBuffer) {
this.clone();
return;
}
- // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari
+ // Detect the mime type of the image directly if it is a Data URL
if (REGEXP_DATA_URL.test(url)) {
+ // Read ArrayBuffer from Data URL of JPEG images directly for better performance
if (REGEXP_DATA_URL_JPEG.test(url)) {
this.read(dataURLToArrayBuffer(url));
} else {
+ // Only a JPEG image may contains Exif Orientation information,
+ // the rest types of Data URLs are not necessary to check orientation at all.
this.clone();
}
return;
}
+ // 1. Detect the mime type of the image by a XMLHttpRequest.
+ // 2. Load the image as ArrayBuffer for reading orientation if its a JPEG image.
const xhr = new XMLHttpRequest();
-
- xhr.onerror = () => {
- this.clone();
+ const clone = this.clone.bind(this);
+
+ this.reloading = true;
+ this.xhr = xhr;
+
+ // 1. Cross origin requests are only supported for protocol schemes:
+ // http, https, data, chrome, chrome-extension.
+ // 2. Access to XMLHttpRequest from a Data URL will be blocked by CORS policy
+ // in some browsers as IE11 and Safari.
+ xhr.onabort = clone;
+ xhr.onerror = clone;
+ xhr.ontimeout = clone;
+
+ xhr.onprogress = () => {
+ // Abort the request directly if it not a JPEG image for better performance
+ if (xhr.getResponseHeader('content-type') !== MIME_TYPE_JPEG) {
+ xhr.abort();
+ }
};
xhr.onload = () => {
this.read(xhr.response);
};
- // Bust cache when there is a "crossOrigin" property
+ xhr.onloadend = () => {
+ this.reloading = false;
+ this.xhr = null;
+ };
+
+ // Bust cache when there is a "crossOrigin" property to avoid browser cache error
if (options.checkCrossOrigin && isCrossOriginURL(url) && element.crossOrigin) {
url = addTimestamp(url);
}
- xhr.open('get', url);
+ xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.withCredentials = element.crossOrigin === 'use-credentials';
xhr.send();
@@ -158,13 +175,17 @@ class Cropper {
read(arrayBuffer) {
const { options, imageData } = this;
- const orientation = getOrientation(arrayBuffer);
+
+ // Reset the orientation value to its default value 1
+ // as some iOS browsers will render image with its orientation
+ const orientation = resetAndGetOrientation(arrayBuffer);
let rotate = 0;
let scaleX = 1;
let scaleY = 1;
if (orientation > 1) {
- this.url = arrayBufferToDataURL(arrayBuffer, 'image/jpeg');
+ // Generate a new URL which has the default orientation value
+ this.url = arrayBufferToDataURL(arrayBuffer, MIME_TYPE_JPEG);
({ rotate, scaleX, scaleY } = parseOrientation(orientation));
}
@@ -182,20 +203,16 @@ class Cropper {
clone() {
const { element, url } = this;
- let crossOrigin;
- let crossOriginUrl;
+ let { crossOrigin } = element;
+ let crossOriginUrl = url;
if (this.options.checkCrossOrigin && isCrossOriginURL(url)) {
- ({ crossOrigin } = element);
-
- if (crossOrigin) {
- crossOriginUrl = url;
- } else {
+ if (!crossOrigin) {
crossOrigin = 'anonymous';
-
- // Bust cache when there is not a "crossOrigin" property
- crossOriginUrl = addTimestamp(url);
}
+
+ // Bust cache when there is not a "crossOrigin" property (#519)
+ crossOriginUrl = addTimestamp(url);
}
this.crossOrigin = crossOrigin;
@@ -208,66 +225,88 @@ class Cropper {
}
image.src = crossOriginUrl || url;
-
- const start = proxy(this.start, this);
- const stop = proxy(this.stop, this);
-
+ image.alt = element.alt || 'The image to crop';
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);
- }
+ image.onload = this.start.bind(this);
+ image.onerror = this.stop.bind(this);
+ addClass(image, CLASS_HIDE);
+ element.parentNode.insertBefore(image, element.nextSibling);
}
- start(event) {
- const image = this.isImg ? this.element : this.image;
+ start() {
+ const { image } = this;
- if (event) {
- removeListener(image, EVENT_LOAD, this.onStart);
- removeListener(image, EVENT_ERROR, this.onStop);
- }
+ image.onload = null;
+ image.onerror = null;
+ this.sizing = true;
- getImageNaturalSizes(image, (naturalWidth, naturalHeight) => {
- extend(this.imageData, {
+ // Match all browsers that use WebKit as the layout engine in iOS devices,
+ // such as Safari for iOS, Chrome for iOS, and in-app browsers.
+ const isIOSWebKit = WINDOW.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WINDOW.navigator.userAgent);
+ const done = (naturalWidth, naturalHeight) => {
+ assign(this.imageData, {
naturalWidth,
naturalHeight,
aspectRatio: naturalWidth / naturalHeight,
});
- this.loaded = true;
+ this.sizing = false;
+ this.sized = true;
this.build();
- });
+ };
+
+ // Most modern browsers (excepts iOS WebKit)
+ if (image.naturalWidth && !isIOSWebKit) {
+ done(image.naturalWidth, image.naturalHeight);
+ return;
+ }
+
+ const sizingImage = document.createElement('img');
+ const body = document.body || document.documentElement;
+
+ this.sizingImage = sizingImage;
+
+ sizingImage.onload = () => {
+ done(sizingImage.width, sizingImage.height);
+
+ if (!isIOSWebKit) {
+ body.removeChild(sizingImage);
+ }
+ };
+
+ sizingImage.src = image.src;
+
+ // iOS WebKit will convert the image automatically
+ // with its orientation once append it into DOM (#279)
+ if (!isIOSWebKit) {
+ sizingImage.style.cssText = (
+ 'left:0;'
+ + 'max-height:none!important;'
+ + 'max-width:none!important;'
+ + 'min-height:0!important;'
+ + 'min-width:0!important;'
+ + 'opacity:0;'
+ + 'position:absolute;'
+ + 'top:0;'
+ + 'z-index:-1;'
+ );
+ body.appendChild(sizingImage);
+ }
}
stop() {
const { image } = this;
- removeListener(image, EVENT_LOAD, this.onStart);
- removeListener(image, EVENT_ERROR, this.onStop);
+ image.onload = null;
+ image.onerror = null;
image.parentNode.removeChild(image);
this.image = null;
}
build() {
- if (!this.loaded) {
+ if (!this.sized || this.ready) {
return;
}
- // Unbuild first when replace
- if (this.ready) {
- this.unbuild();
- }
-
const { element, options, image } = this;
// Create cropper elements
@@ -306,18 +345,11 @@ class Cropper {
this.initPreview();
this.bind();
+ options.initialAspectRatio = Math.max(0, options.initialAspectRatio) || NaN;
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);
- }
+ addClass(cropBox, CLASS_HIDDEN);
if (!options.guides) {
addClass(cropBox.getElementsByClassName(`${NAMESPACE}-dashed`), CLASS_HIDDEN);
@@ -345,24 +377,23 @@ class Cropper {
addClass(cropBox.getElementsByClassName(`${NAMESPACE}-point`), CLASS_HIDDEN);
}
- this.setDragMode(options.dragMode);
this.render();
this.ready = true;
- this.setData(options.data);
+ this.setDragMode(options.dragMode);
- // 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,
- });
- }
+ if (options.autoCrop) {
+ this.crop();
+ }
+
+ this.setData(options.data);
- dispatchEvent(element, EVENT_READY);
- dispatchEvent(element, EVENT_CROP, this.getData());
+ if (isFunction(options.ready)) {
+ addListener(element, EVENT_READY, options.ready, {
+ once: true,
+ });
+ }
- this.complete = true;
- }, 0);
+ dispatchEvent(element, EVENT_READY);
}
unbuild() {
@@ -370,32 +401,28 @@ class Cropper {
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;
+ removeClass(this.element, CLASS_HIDDEN);
+ }
+
+ uncreate() {
+ if (this.ready) {
+ this.unbuild();
+ this.ready = false;
+ this.cropped = false;
+ } else if (this.sizing) {
+ this.sizingImage.onload = null;
+ this.sizing = false;
+ this.sized = false;
+ } else if (this.reloading) {
+ this.xhr.onabort = null;
+ this.xhr.abort();
+ } else if (this.image) {
+ this.stop();
+ }
}
/**
@@ -412,10 +439,10 @@ class Cropper {
* @param {Object} options - The new default options.
*/
static setDefaults(options) {
- extend(DEFAULTS, isPlainObject(options) && options);
+ assign(DEFAULTS, isPlainObject(options) && options);
}
}
-extend(Cropper.prototype, render, preview, events, handlers, change, methods);
+assign(Cropper.prototype, render, preview, events, handlers, change, methods);
export default Cropper;