aboutsummaryrefslogtreecommitdiffstats
path: root/library/cropperjs/src
diff options
context:
space:
mode:
Diffstat (limited to 'library/cropperjs/src')
-rw-r--r--library/cropperjs/src/css/cropper.css46
-rw-r--r--library/cropperjs/src/css/cropper.scss286
-rw-r--r--library/cropperjs/src/index.css1
-rw-r--r--library/cropperjs/src/index.js3
-rw-r--r--library/cropperjs/src/index.scss1
-rw-r--r--library/cropperjs/src/js/change.js128
-rw-r--r--library/cropperjs/src/js/constants.js34
-rw-r--r--library/cropperjs/src/js/cropper.js275
-rw-r--r--library/cropperjs/src/js/defaults.js7
-rw-r--r--library/cropperjs/src/js/events.js21
-rw-r--r--library/cropperjs/src/js/handlers.js100
-rw-r--r--library/cropperjs/src/js/methods.js156
-rw-r--r--library/cropperjs/src/js/preview.js66
-rw-r--r--library/cropperjs/src/js/render.js65
-rw-r--r--library/cropperjs/src/js/template.js50
-rw-r--r--library/cropperjs/src/js/utilities.js524
16 files changed, 1056 insertions, 707 deletions
diff --git a/library/cropperjs/src/css/cropper.css b/library/cropperjs/src/css/cropper.css
index d09c4f182..756a7e1c1 100644
--- a/library/cropperjs/src/css/cropper.css
+++ b/library/cropperjs/src/css/cropper.css
@@ -1,7 +1,3 @@
-:root {
- --blue: #39f;
-}
-
.cropper-container {
direction: ltr;
font-size: 0;
@@ -11,7 +7,6 @@
user-select: none;
& img {
- /* Avoid margin top issue (Occur only when margin-top <= -height) */
display: block;
height: 100%;
image-orientation: 0deg;
@@ -47,14 +42,14 @@
.cropper-modal {
background-color: #000;
- opacity: .5;
+ opacity: 0.5;
}
.cropper-view-box {
display: block;
height: 100%;
- outline-color: color(var(--blue) alpha(75%));
- outline: 1px solid var(--blue);
+ outline: 1px solid #39f;
+ outline-color: rgba(51, 153, 255, 0.75);
overflow: hidden;
width: 100%;
}
@@ -62,15 +57,15 @@
.cropper-dashed {
border: 0 dashed #eee;
display: block;
- opacity: .5;
+ opacity: 0.5;
position: absolute;
&.dashed-h {
border-bottom-width: 1px;
border-top-width: 1px;
- height: calc(100 / 3)%;
+ height: calc(100% / 3);
left: 0;
- top: calc(100 / 3)%;
+ top: calc(100% / 3);
width: 100%;
}
@@ -78,9 +73,9 @@
border-left-width: 1px;
border-right-width: 1px;
height: 100%;
- left: calc(100 / 3)%;
+ left: calc(100% / 3);
top: 0;
- width: calc(100 / 3)%;
+ width: calc(100% / 3);
}
}
@@ -88,27 +83,27 @@
display: block;
height: 0;
left: 50%;
- opacity: .75;
+ opacity: 0.75;
position: absolute;
top: 50%;
width: 0;
- &:before,
- &:after {
+ &::before,
+ &::after {
background-color: #eee;
content: ' ';
display: block;
position: absolute;
}
- &:before {
+ &::before {
height: 1px;
left: -3px;
top: 0;
width: 7px;
}
- &:after {
+ &::after {
height: 7px;
left: 0;
top: -3px;
@@ -121,7 +116,7 @@
.cropper-point {
display: block;
height: 100%;
- opacity: .1;
+ opacity: 0.1;
position: absolute;
width: 100%;
}
@@ -133,7 +128,7 @@
}
.cropper-line {
- background-color: var(--blue);
+ background-color: #39f;
&.line-e {
cursor: ew-resize;
@@ -165,9 +160,9 @@
}
.cropper-point {
- background-color: var(--blue);
+ background-color: #39f;
height: 5px;
- opacity: .75;
+ opacity: 0.75;
width: 5px;
&.point-e {
@@ -236,13 +231,13 @@
@media (min-width: 1200px) {
height: 5px;
- opacity: .75;
+ opacity: 0.75;
width: 5px;
}
}
- &.point-se:before {
- background-color: var(--blue);
+ &.point-se::before {
+ background-color: #39f;
bottom: -50%;
content: ' ';
display: block;
@@ -287,4 +282,3 @@
.cropper-disabled .cropper-point {
cursor: not-allowed;
}
-
diff --git a/library/cropperjs/src/css/cropper.scss b/library/cropperjs/src/css/cropper.scss
new file mode 100644
index 000000000..cfca464b7
--- /dev/null
+++ b/library/cropperjs/src/css/cropper.scss
@@ -0,0 +1,286 @@
+.cropper {
+ &-container {
+ direction: ltr;
+ font-size: 0;
+ line-height: 0;
+ position: relative;
+ touch-action: none;
+ user-select: none;
+
+ img {
+ display: block;
+ height: 100%;
+ image-orientation: 0deg;
+ max-height: none !important;
+ max-width: none !important;
+ min-height: 0 !important;
+ min-width: 0 !important;
+ width: 100%;
+ }
+ }
+
+ &-wrap-box,
+ &-canvas,
+ &-drag-box,
+ &-crop-box,
+ &-modal {
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+
+ &-wrap-box,
+ &-canvas {
+ overflow: hidden;
+ }
+
+ &-drag-box {
+ background-color: #fff;
+ opacity: 0;
+ }
+
+ &-modal {
+ background-color: #000;
+ opacity: 0.5;
+ }
+
+ &-view-box {
+ display: block;
+ height: 100%;
+ outline: 1px solid #39f;
+ outline-color: rgba(51, 153, 255, 0.75);
+ overflow: hidden;
+ width: 100%;
+ }
+
+ &-dashed {
+ border: 0 dashed #eee;
+ display: block;
+ opacity: 0.5;
+ position: absolute;
+
+ &.dashed-h {
+ border-bottom-width: 1px;
+ border-top-width: 1px;
+ height: calc(100% / 3);
+ left: 0;
+ top: calc(100% / 3);
+ width: 100%;
+ }
+
+ &.dashed-v {
+ border-left-width: 1px;
+ border-right-width: 1px;
+ height: 100%;
+ left: calc(100% / 3);
+ top: 0;
+ width: calc(100% / 3);
+ }
+ }
+
+ &-center {
+ display: block;
+ height: 0;
+ left: 50%;
+ opacity: 0.75;
+ position: absolute;
+ top: 50%;
+ width: 0;
+
+ &::before,
+ &::after {
+ background-color: #eee;
+ content: ' ';
+ display: block;
+ position: absolute;
+ }
+
+ &::before {
+ height: 1px;
+ left: -3px;
+ top: 0;
+ width: 7px;
+ }
+
+ &::after {
+ height: 7px;
+ left: 0;
+ top: -3px;
+ width: 1px;
+ }
+ }
+
+ &-face,
+ &-line,
+ &-point {
+ display: block;
+ height: 100%;
+ opacity: 0.1;
+ position: absolute;
+ width: 100%;
+ }
+
+ &-face {
+ background-color: #fff;
+ left: 0;
+ top: 0;
+ }
+
+ &-line {
+ background-color: #39f;
+
+ &.line-e {
+ cursor: ew-resize;
+ right: -3px;
+ top: 0;
+ width: 5px;
+ }
+
+ &.line-n {
+ cursor: ns-resize;
+ height: 5px;
+ left: 0;
+ top: -3px;
+ }
+
+ &.line-w {
+ cursor: ew-resize;
+ left: -3px;
+ top: 0;
+ width: 5px;
+ }
+
+ &.line-s {
+ bottom: -3px;
+ cursor: ns-resize;
+ height: 5px;
+ left: 0;
+ }
+ }
+
+ &-point {
+ background-color: #39f;
+ height: 5px;
+ opacity: 0.75;
+ width: 5px;
+
+ &.point-e {
+ cursor: ew-resize;
+ margin-top: -3px;
+ right: -3px;
+ top: 50%;
+ }
+
+ &.point-n {
+ cursor: ns-resize;
+ left: 50%;
+ margin-left: -3px;
+ top: -3px;
+ }
+
+ &.point-w {
+ cursor: ew-resize;
+ left: -3px;
+ margin-top: -3px;
+ top: 50%;
+ }
+
+ &.point-s {
+ bottom: -3px;
+ cursor: s-resize;
+ left: 50%;
+ margin-left: -3px;
+ }
+
+ &.point-ne {
+ cursor: nesw-resize;
+ right: -3px;
+ top: -3px;
+ }
+
+ &.point-nw {
+ cursor: nwse-resize;
+ left: -3px;
+ top: -3px;
+ }
+
+ &.point-sw {
+ bottom: -3px;
+ cursor: nesw-resize;
+ left: -3px;
+ }
+
+ &.point-se {
+ bottom: -3px;
+ cursor: nwse-resize;
+ height: 20px;
+ opacity: 1;
+ right: -3px;
+ width: 20px;
+
+ @media (min-width: 768px) {
+ height: 15px;
+ width: 15px;
+ }
+
+ @media (min-width: 992px) {
+ height: 10px;
+ width: 10px;
+ }
+
+ @media (min-width: 1200px) {
+ height: 5px;
+ opacity: 0.75;
+ width: 5px;
+ }
+ }
+
+ &.point-se::before {
+ background-color: #39f;
+ bottom: -50%;
+ content: ' ';
+ display: block;
+ height: 200%;
+ opacity: 0;
+ position: absolute;
+ right: -50%;
+ width: 200%;
+ }
+ }
+
+ &-invisible {
+ opacity: 0;
+ }
+
+ &-bg {
+ background-image: url('../images/bg.png');
+ }
+
+ &-hide {
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+ }
+
+ &-hidden {
+ display: none !important;
+ }
+
+ &-move {
+ cursor: move;
+ }
+
+ &-crop {
+ cursor: crosshair;
+ }
+
+ &-disabled &-drag-box,
+ &-disabled &-face,
+ &-disabled &-line,
+ &-disabled &-point {
+ cursor: not-allowed;
+ }
+}
diff --git a/library/cropperjs/src/index.css b/library/cropperjs/src/index.css
new file mode 100644
index 000000000..ac94e343e
--- /dev/null
+++ b/library/cropperjs/src/index.css
@@ -0,0 +1 @@
+@import './css/cropper.css';
diff --git a/library/cropperjs/src/index.js b/library/cropperjs/src/index.js
new file mode 100644
index 000000000..ac940610f
--- /dev/null
+++ b/library/cropperjs/src/index.js
@@ -0,0 +1,3 @@
+import Cropper from './js/cropper';
+
+export default Cropper;
diff --git a/library/cropperjs/src/index.scss b/library/cropperjs/src/index.scss
new file mode 100644
index 000000000..90695c59a
--- /dev/null
+++ b/library/cropperjs/src/index.scss
@@ -0,0 +1 @@
+@import './css/cropper.scss';
diff --git a/library/cropperjs/src/js/change.js b/library/cropperjs/src/js/change.js
index 014222ef0..c0ddf27ee 100644
--- a/library/cropperjs/src/js/change.js
+++ b/library/cropperjs/src/js/change.js
@@ -14,14 +14,14 @@ import {
CLASS_HIDDEN,
} from './constants';
import {
- each,
+ forEach,
getMaxZoomRatio,
getOffset,
removeClass,
} from './utilities';
export default {
- change(e) {
+ change(event) {
const {
options,
canvasData,
@@ -47,7 +47,7 @@ export default {
let offset;
// Locking aspect ratio in "free mode" by holding shift key
- if (!aspectRatio && e.shiftKey) {
+ if (!aspectRatio && event.shiftKey) {
aspectRatio = width && height ? width / height : 1;
}
@@ -113,8 +113,8 @@ export default {
// Resize crop box
case ACTION_EAST:
- if (range.x >= 0 && (right >= maxWidth || (aspectRatio &&
- (top <= minTop || bottom >= maxHeight)))) {
+ if (range.x >= 0 && (right >= maxWidth || (aspectRatio
+ && (top <= minTop || bottom >= maxHeight)))) {
renderable = false;
break;
}
@@ -122,21 +122,22 @@ export default {
check(ACTION_EAST);
width += range.x;
- if (aspectRatio) {
- height = width / aspectRatio;
- top -= (range.x / aspectRatio) / 2;
- }
-
if (width < 0) {
action = ACTION_WEST;
- width = 0;
+ width = -width;
+ left -= width;
+ }
+
+ if (aspectRatio) {
+ height = width / aspectRatio;
+ top += (cropBoxData.height - height) / 2;
}
break;
case ACTION_NORTH:
- if (range.y <= 0 && (top <= minTop || (aspectRatio &&
- (left <= minLeft || right >= maxWidth)))) {
+ if (range.y <= 0 && (top <= minTop || (aspectRatio
+ && (left <= minLeft || right >= maxWidth)))) {
renderable = false;
break;
}
@@ -145,21 +146,22 @@ export default {
height -= range.y;
top += range.y;
- if (aspectRatio) {
- width = height * aspectRatio;
- left += (range.y * aspectRatio) / 2;
- }
-
if (height < 0) {
action = ACTION_SOUTH;
- height = 0;
+ height = -height;
+ top -= height;
+ }
+
+ if (aspectRatio) {
+ width = height * aspectRatio;
+ left += (cropBoxData.width - width) / 2;
}
break;
case ACTION_WEST:
- if (range.x <= 0 && (left <= minLeft || (aspectRatio &&
- (top <= minTop || bottom >= maxHeight)))) {
+ if (range.x <= 0 && (left <= minLeft || (aspectRatio
+ && (top <= minTop || bottom >= maxHeight)))) {
renderable = false;
break;
}
@@ -168,21 +170,22 @@ export default {
width -= range.x;
left += range.x;
- if (aspectRatio) {
- height = width / aspectRatio;
- top += (range.x / aspectRatio) / 2;
- }
-
if (width < 0) {
action = ACTION_EAST;
- width = 0;
+ width = -width;
+ left -= width;
+ }
+
+ if (aspectRatio) {
+ height = width / aspectRatio;
+ top += (cropBoxData.height - height) / 2;
}
break;
case ACTION_SOUTH:
- if (range.y >= 0 && (bottom >= maxHeight || (aspectRatio &&
- (left <= minLeft || right >= maxWidth)))) {
+ if (range.y >= 0 && (bottom >= maxHeight || (aspectRatio
+ && (left <= minLeft || right >= maxWidth)))) {
renderable = false;
break;
}
@@ -190,14 +193,15 @@ export default {
check(ACTION_SOUTH);
height += range.y;
- if (aspectRatio) {
- width = height * aspectRatio;
- left -= (range.y * aspectRatio) / 2;
- }
-
if (height < 0) {
action = ACTION_NORTH;
- height = 0;
+ height = -height;
+ top -= height;
+ }
+
+ if (aspectRatio) {
+ width = height * aspectRatio;
+ left += (cropBoxData.width - width) / 2;
}
break;
@@ -240,14 +244,18 @@ export default {
if (width < 0 && height < 0) {
action = ACTION_SOUTH_WEST;
- height = 0;
- width = 0;
+ height = -height;
+ width = -width;
+ top -= height;
+ left -= width;
} else if (width < 0) {
action = ACTION_NORTH_WEST;
- width = 0;
+ width = -width;
+ left -= width;
} else if (height < 0) {
action = ACTION_SOUTH_EAST;
- height = 0;
+ height = -height;
+ top -= height;
}
break;
@@ -263,7 +271,7 @@ export default {
height -= range.y;
top += range.y;
width = height * aspectRatio;
- left += range.y * aspectRatio;
+ left += cropBoxData.width - width;
} else {
check(ACTION_NORTH);
check(ACTION_WEST);
@@ -293,14 +301,18 @@ export default {
if (width < 0 && height < 0) {
action = ACTION_SOUTH_EAST;
- height = 0;
- width = 0;
+ height = -height;
+ width = -width;
+ top -= height;
+ left -= width;
} else if (width < 0) {
action = ACTION_NORTH_EAST;
- width = 0;
+ width = -width;
+ left -= width;
} else if (height < 0) {
action = ACTION_SOUTH_WEST;
- height = 0;
+ height = -height;
+ top -= height;
}
break;
@@ -343,14 +355,18 @@ export default {
if (width < 0 && height < 0) {
action = ACTION_NORTH_EAST;
- height = 0;
- width = 0;
+ height = -height;
+ width = -width;
+ top -= height;
+ left -= width;
} else if (width < 0) {
action = ACTION_SOUTH_EAST;
- width = 0;
+ width = -width;
+ left -= width;
} else if (height < 0) {
action = ACTION_NORTH_WEST;
- height = 0;
+ height = -height;
+ top -= height;
}
break;
@@ -390,14 +406,18 @@ export default {
if (width < 0 && height < 0) {
action = ACTION_NORTH_WEST;
- height = 0;
- width = 0;
+ height = -height;
+ width = -width;
+ top -= height;
+ left -= width;
} else if (width < 0) {
action = ACTION_SOUTH_WEST;
- width = 0;
+ width = -width;
+ left -= width;
} else if (height < 0) {
action = ACTION_NORTH_EAST;
- height = 0;
+ height = -height;
+ top -= height;
}
break;
@@ -410,7 +430,7 @@ export default {
// Zoom canvas
case ACTION_ZOOM:
- this.zoom(getMaxZoomRatio(pointers), e);
+ this.zoom(getMaxZoomRatio(pointers), event);
renderable = false;
break;
@@ -463,7 +483,7 @@ export default {
}
// Override
- each(pointers, (p) => {
+ forEach(pointers, (p) => {
p.startX = p.endX;
p.startY = p.endY;
});
diff --git a/library/cropperjs/src/js/constants.js b/library/cropperjs/src/js/constants.js
index c675d4d1c..015fcbcdd 100644
--- a/library/cropperjs/src/js/constants.js
+++ b/library/cropperjs/src/js/constants.js
@@ -1,4 +1,7 @@
-export const WINDOW = typeof window !== 'undefined' ? window : {};
+export const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
+export const WINDOW = IS_BROWSER ? window : {};
+export const IS_TOUCH_DEVICE = IS_BROWSER ? 'ontouchstart' in WINDOW.document.documentElement : false;
+export const HAS_POINTER_EVENT = IS_BROWSER ? 'PointerEvent' in WINDOW : false;
export const NAMESPACE = 'cropper';
// Actions
@@ -25,8 +28,8 @@ export const CLASS_MODAL = `${NAMESPACE}-modal`;
export const CLASS_MOVE = `${NAMESPACE}-move`;
// Data keys
-export const DATA_ACTION = 'action';
-export const DATA_PREVIEW = 'preview';
+export const DATA_ACTION = `${NAMESPACE}Action`;
+export const DATA_PREVIEW = `${NAMESPACE}Preview`;
// Drag modes
export const DRAG_MODE_CROP = 'crop';
@@ -39,18 +42,27 @@ export const EVENT_CROP_END = 'cropend';
export const EVENT_CROP_MOVE = 'cropmove';
export const EVENT_CROP_START = 'cropstart';
export const EVENT_DBLCLICK = 'dblclick';
-export const EVENT_ERROR = 'error';
-export const EVENT_LOAD = 'load';
-export const EVENT_POINTER_DOWN = WINDOW.PointerEvent ? 'pointerdown' : 'touchstart mousedown';
-export const EVENT_POINTER_MOVE = WINDOW.PointerEvent ? 'pointermove' : 'touchmove mousemove';
-export const EVENT_POINTER_UP = WINDOW.PointerEvent ? 'pointerup pointercancel' : 'touchend touchcancel mouseup';
+export const EVENT_TOUCH_START = IS_TOUCH_DEVICE ? 'touchstart' : 'mousedown';
+export const EVENT_TOUCH_MOVE = IS_TOUCH_DEVICE ? 'touchmove' : 'mousemove';
+export const EVENT_TOUCH_END = IS_TOUCH_DEVICE ? 'touchend touchcancel' : 'mouseup';
+export const EVENT_POINTER_DOWN = HAS_POINTER_EVENT ? 'pointerdown' : EVENT_TOUCH_START;
+export const EVENT_POINTER_MOVE = HAS_POINTER_EVENT ? 'pointermove' : EVENT_TOUCH_MOVE;
+export const EVENT_POINTER_UP = HAS_POINTER_EVENT ? 'pointerup pointercancel' : EVENT_TOUCH_END;
export const EVENT_READY = 'ready';
export const EVENT_RESIZE = 'resize';
-export const EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll';
+export const EVENT_WHEEL = 'wheel';
export const EVENT_ZOOM = 'zoom';
+// Mime types
+export const MIME_TYPE_JPEG = 'image/jpeg';
+
// RegExps
-export const REGEXP_ACTIONS = /^(?:e|w|s|n|se|sw|ne|nw|all|crop|move|zoom)$/;
+export const REGEXP_ACTIONS = /^e|w|s|n|se|sw|ne|nw|all|crop|move|zoom$/;
export const REGEXP_DATA_URL = /^data:/;
export const REGEXP_DATA_URL_JPEG = /^data:image\/jpeg;base64,/;
-export const REGEXP_TAG_NAME = /^(?:img|canvas)$/i;
+export const REGEXP_TAG_NAME = /^img|canvas$/i;
+
+// Misc
+// Inspired by the default width and height of a canvas element.
+export const MIN_CONTAINER_WIDTH = 200;
+export const MIN_CONTAINER_HEIGHT = 100;
diff --git a/library/cropperjs/src/js/cropper.js b/library/cropperjs/src/js/cropper.js
index 97f3511df..24bf81426 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;
@@ -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;
diff --git a/library/cropperjs/src/js/defaults.js b/library/cropperjs/src/js/defaults.js
index aa469e73a..ce6ab9698 100644
--- a/library/cropperjs/src/js/defaults.js
+++ b/library/cropperjs/src/js/defaults.js
@@ -1,6 +1,4 @@
-import {
- DRAG_MODE_CROP,
-} from './constants';
+import { DRAG_MODE_CROP } from './constants';
export default {
// Define the view mode of the cropper
@@ -9,6 +7,9 @@ export default {
// Define the dragging mode of the cropper
dragMode: DRAG_MODE_CROP, // 'crop', 'move' or 'none'
+ // Define the initial aspect ratio of the crop box
+ initialAspectRatio: NaN,
+
// Define the aspect ratio of the crop box
aspectRatio: NaN,
diff --git a/library/cropperjs/src/js/events.js b/library/cropperjs/src/js/events.js
index 3753db022..e46888704 100644
--- a/library/cropperjs/src/js/events.js
+++ b/library/cropperjs/src/js/events.js
@@ -14,7 +14,6 @@ import {
import {
addListener,
isFunction,
- proxy,
removeListener,
} from './utilities';
@@ -42,29 +41,32 @@ export default {
addListener(element, EVENT_ZOOM, options.zoom);
}
- addListener(cropper, EVENT_POINTER_DOWN, (this.onCropStart = proxy(this.cropStart, this)));
+ addListener(cropper, EVENT_POINTER_DOWN, (this.onCropStart = this.cropStart.bind(this)));
if (options.zoomable && options.zoomOnWheel) {
- addListener(cropper, EVENT_WHEEL, (this.onWheel = proxy(this.wheel, this)));
+ addListener(cropper, EVENT_WHEEL, (this.onWheel = this.wheel.bind(this)), {
+ passive: false,
+ capture: true,
+ });
}
if (options.toggleDragModeOnDblclick) {
- addListener(cropper, EVENT_DBLCLICK, (this.onDblclick = proxy(this.dblclick, this)));
+ addListener(cropper, EVENT_DBLCLICK, (this.onDblclick = this.dblclick.bind(this)));
}
addListener(
element.ownerDocument,
EVENT_POINTER_MOVE,
- (this.onCropMove = proxy(this.cropMove, this)),
+ (this.onCropMove = this.cropMove.bind(this)),
);
addListener(
element.ownerDocument,
EVENT_POINTER_UP,
- (this.onCropEnd = proxy(this.cropEnd, this)),
+ (this.onCropEnd = this.cropEnd.bind(this)),
);
if (options.responsive) {
- addListener(window, EVENT_RESIZE, (this.onResize = proxy(this.resize, this)));
+ addListener(window, EVENT_RESIZE, (this.onResize = this.resize.bind(this)));
}
},
@@ -94,7 +96,10 @@ export default {
removeListener(cropper, EVENT_POINTER_DOWN, this.onCropStart);
if (options.zoomable && options.zoomOnWheel) {
- removeListener(cropper, EVENT_WHEEL, this.onWheel);
+ removeListener(cropper, EVENT_WHEEL, this.onWheel, {
+ passive: false,
+ capture: true,
+ });
}
if (options.toggleDragModeOnDblclick) {
diff --git a/library/cropperjs/src/js/handlers.js b/library/cropperjs/src/js/handlers.js
index 7b7b2469a..43f999767 100644
--- a/library/cropperjs/src/js/handlers.js
+++ b/library/cropperjs/src/js/handlers.js
@@ -10,27 +10,30 @@ import {
EVENT_CROP_END,
EVENT_CROP_MOVE,
EVENT_CROP_START,
+ MIN_CONTAINER_WIDTH,
+ MIN_CONTAINER_HEIGHT,
REGEXP_ACTIONS,
} from './constants';
import {
addClass,
+ assign,
dispatchEvent,
- each,
- extend,
+ forEach,
getData,
getPointer,
hasClass,
+ isNumber,
toggleClass,
} from './utilities';
export default {
resize() {
const { options, container, containerData } = this;
- const minContainerWidth = Number(options.minContainerWidth) || 200;
- const minContainerHeight = Number(options.minContainerHeight) || 100;
+ const minContainerWidth = Number(options.minContainerWidth) || MIN_CONTAINER_WIDTH;
+ const minContainerHeight = Number(options.minContainerHeight) || MIN_CONTAINER_HEIGHT;
- if (this.disabled || containerData.width <= minContainerWidth ||
- containerData.height <= minContainerHeight) {
+ if (this.disabled || containerData.width <= minContainerWidth
+ || containerData.height <= minContainerHeight) {
return;
}
@@ -49,10 +52,10 @@ export default {
this.render();
if (options.restore) {
- this.setCanvasData(each(canvasData, (n, i) => {
+ this.setCanvasData(forEach(canvasData, (n, i) => {
canvasData[i] = n * ratio;
}));
- this.setCropBoxData(each(cropBoxData, (n, i) => {
+ this.setCropBoxData(forEach(cropBoxData, (n, i) => {
cropBoxData[i] = n * ratio;
}));
}
@@ -67,7 +70,7 @@ export default {
this.setDragMode(hasClass(this.dragBox, CLASS_CROP) ? DRAG_MODE_MOVE : DRAG_MODE_CROP);
},
- wheel(e) {
+ wheel(event) {
const ratio = Number(this.options.wheelZoomRatio) || 0.1;
let delta = 1;
@@ -75,7 +78,7 @@ export default {
return;
}
- e.preventDefault();
+ event.preventDefault();
// Limit wheel speed to prevent zoom too fast (#21)
if (this.wheeling) {
@@ -88,39 +91,56 @@ export default {
this.wheeling = false;
}, 50);
- if (e.deltaY) {
- delta = e.deltaY > 0 ? 1 : -1;
- } else if (e.wheelDelta) {
- delta = -e.wheelDelta / 120;
- } else if (e.detail) {
- delta = e.detail > 0 ? 1 : -1;
+ if (event.deltaY) {
+ delta = event.deltaY > 0 ? 1 : -1;
+ } else if (event.wheelDelta) {
+ delta = -event.wheelDelta / 120;
+ } else if (event.detail) {
+ delta = event.detail > 0 ? 1 : -1;
}
- this.zoom(-delta * ratio, e);
+ this.zoom(-delta * ratio, event);
},
- cropStart(e) {
- if (this.disabled) {
+ cropStart(event) {
+ const { buttons, button } = event;
+
+ if (
+ this.disabled
+
+ // Handle mouse event and pointer event and ignore touch event
+ || ((
+ event.type === 'mousedown'
+ || (event.type === 'pointerdown' && event.pointerType === 'mouse')
+ ) && (
+ // No primary button (Usually the left button)
+ (isNumber(buttons) && buttons !== 1)
+ || (isNumber(button) && button !== 0)
+
+ // Open context menu
+ || event.ctrlKey
+ ))
+ ) {
return;
}
const { options, pointers } = this;
let action;
- if (e.changedTouches) {
+ if (event.changedTouches) {
// Handle touch event
- each(e.changedTouches, (touch) => {
+ forEach(event.changedTouches, (touch) => {
pointers[touch.identifier] = getPointer(touch);
});
} else {
// Handle mouse event and pointer event
- pointers[e.pointerId || 0] = getPointer(e);
+ pointers[event.pointerId || 0] = getPointer(event);
}
if (Object.keys(pointers).length > 1 && options.zoomable && options.zoomOnTouch) {
action = ACTION_ZOOM;
} else {
- action = getData(e.target, DATA_ACTION);
+ action = getData(event.target, DATA_ACTION);
}
if (!REGEXP_ACTIONS.test(action)) {
@@ -128,13 +148,14 @@ export default {
}
if (dispatchEvent(this.element, EVENT_CROP_START, {
- originalEvent: e,
+ originalEvent: event,
action,
}) === false) {
return;
}
- e.preventDefault();
+ // This line is required for preventing page zooming in iOS browsers
+ event.preventDefault();
this.action = action;
this.cropping = false;
@@ -145,7 +166,7 @@ export default {
}
},
- cropMove(e) {
+ cropMove(event) {
const { action } = this;
if (this.disabled || !action) {
@@ -154,46 +175,47 @@ export default {
const { pointers } = this;
- e.preventDefault();
+ event.preventDefault();
if (dispatchEvent(this.element, EVENT_CROP_MOVE, {
- originalEvent: e,
+ originalEvent: event,
action,
}) === false) {
return;
}
- if (e.changedTouches) {
- each(e.changedTouches, (touch) => {
- extend(pointers[touch.identifier], getPointer(touch, true));
+ if (event.changedTouches) {
+ forEach(event.changedTouches, (touch) => {
+ // The first parameter should not be undefined (#432)
+ assign(pointers[touch.identifier] || {}, getPointer(touch, true));
});
} else {
- extend(pointers[e.pointerId || 0], getPointer(e, true));
+ assign(pointers[event.pointerId || 0] || {}, getPointer(event, true));
}
- this.change(e);
+ this.change(event);
},
- cropEnd(e) {
+ cropEnd(event) {
if (this.disabled) {
return;
}
const { action, pointers } = this;
- if (e.changedTouches) {
- each(e.changedTouches, (touch) => {
+ if (event.changedTouches) {
+ forEach(event.changedTouches, (touch) => {
delete pointers[touch.identifier];
});
} else {
- delete pointers[e.pointerId || 0];
+ delete pointers[event.pointerId || 0];
}
if (!action) {
return;
}
- e.preventDefault();
+ event.preventDefault();
if (!Object.keys(pointers).length) {
this.action = '';
@@ -205,7 +227,7 @@ export default {
}
dispatchEvent(this.element, EVENT_CROP_END, {
- originalEvent: e,
+ originalEvent: event,
action,
});
},
diff --git a/library/cropperjs/src/js/methods.js b/library/cropperjs/src/js/methods.js
index 3627d2bd1..574ac2a71 100644
--- a/library/cropperjs/src/js/methods.js
+++ b/library/cropperjs/src/js/methods.js
@@ -8,27 +8,23 @@ import {
DRAG_MODE_CROP,
DRAG_MODE_MOVE,
DRAG_MODE_NONE,
- EVENT_LOAD,
EVENT_ZOOM,
NAMESPACE,
} from './constants';
import {
addClass,
+ assign,
dispatchEvent,
- each,
- extend,
+ forEach,
getAdjustedSizes,
getOffset,
getPointersCenter,
getSourceCanvas,
- isFunction,
isNumber,
isPlainObject,
isUndefined,
normalizeDecimalNumber,
removeClass,
- removeData,
- removeListener,
setData,
toggleClass,
} from './utilities';
@@ -36,18 +32,15 @@ import {
export default {
// Show the crop box manually
crop() {
- if (this.ready && !this.disabled) {
- if (!this.cropped) {
- this.cropped = true;
- this.limitCropBox(true, true);
-
- if (this.options.modal) {
- addClass(this.dragBox, CLASS_MODAL);
- }
+ if (this.ready && !this.cropped && !this.disabled) {
+ this.cropped = true;
+ this.limitCropBox(true, true);
- removeClass(this.cropBox, CLASS_HIDDEN);
+ if (this.options.modal) {
+ addClass(this.dragBox, CLASS_MODAL);
}
+ removeClass(this.cropBox, CLASS_HIDDEN);
this.setCropBoxData(this.initialCropBoxData);
}
@@ -57,9 +50,9 @@ export default {
// Reset the image and crop box to their initial states
reset() {
if (this.ready && !this.disabled) {
- this.imageData = extend({}, this.initialImageData);
- this.canvasData = extend({}, this.initialCanvasData);
- this.cropBoxData = extend({}, this.initialCropBoxData);
+ this.imageData = assign({}, this.initialImageData);
+ this.canvasData = assign({}, this.initialCanvasData);
+ this.cropBoxData = assign({}, this.initialCropBoxData);
this.renderCanvas();
if (this.cropped) {
@@ -73,7 +66,7 @@ export default {
// Clear the crop box
clear() {
if (this.cropped && !this.disabled) {
- extend(this.cropBoxData, {
+ assign(this.cropBoxData, {
left: 0,
top: 0,
width: 0,
@@ -96,23 +89,23 @@ export default {
/**
* Replace the image's src and rebuild the cropper
* @param {string} url - The new URL.
- * @param {boolean} [onlyColorChanged] - Indicate if the new image only changed color.
- * @returns {Object} this
+ * @param {boolean} [hasSameSize] - Indicate if the new image has the same size as the old one.
+ * @returns {Cropper} this
*/
- replace(url, onlyColorChanged = false) {
+ replace(url, hasSameSize = false) {
if (!this.disabled && url) {
if (this.isImg) {
this.element.src = url;
}
- if (onlyColorChanged) {
+ if (hasSameSize) {
this.url = url;
this.image.src = url;
if (this.ready) {
- this.image2.src = url;
+ this.viewBoxImage.src = url;
- each(this.previews, (element) => {
+ forEach(this.previews, (element) => {
element.getElementsByTagName('img')[0].src = url;
});
}
@@ -121,8 +114,8 @@ export default {
this.replaced = true;
}
- // Clear previous data
this.options.data = null;
+ this.uncreate();
this.load(url);
}
}
@@ -132,7 +125,7 @@ export default {
// Enable (unfreeze) the cropper
enable() {
- if (this.ready) {
+ if (this.ready && this.disabled) {
this.disabled = false;
removeClass(this.cropper, CLASS_DISABLED);
}
@@ -142,7 +135,7 @@ export default {
// Disable (freeze) the cropper
disable() {
- if (this.ready) {
+ if (this.ready && !this.disabled) {
this.disabled = true;
addClass(this.cropper, CLASS_DISABLED);
}
@@ -150,35 +143,34 @@ export default {
return this;
},
- // Destroy the cropper and remove the instance from the image
+ /**
+ * Destroy the cropper and remove the instance from the image
+ * @returns {Cropper} this
+ */
destroy() {
- const { element, image } = this;
-
- if (this.loaded) {
- if (this.isImg && this.replaced) {
- element.src = this.originalUrl;
- }
+ const { element } = this;
- this.unbuild();
- removeClass(element, CLASS_HIDDEN);
- } else if (this.isImg) {
- removeListener(element, EVENT_LOAD, this.onStart);
- } else if (image) {
- image.parentNode.removeChild(image);
+ if (!element[NAMESPACE]) {
+ return this;
}
- removeData(element, NAMESPACE);
+ element[NAMESPACE] = undefined;
+ if (this.isImg && this.replaced) {
+ element.src = this.originalUrl;
+ }
+
+ this.uncreate();
return this;
},
/**
* Move the canvas with relative offsets
* @param {number} offsetX - The relative offset distance on the x-axis.
- * @param {number} offsetY - The relative offset distance on the y-axis.
- * @returns {Object} this
+ * @param {number} [offsetY=offsetX] - The relative offset distance on the y-axis.
+ * @returns {Cropper} this
*/
- move(offsetX, offsetY) {
+ move(offsetX, offsetY = offsetX) {
const { left, top } = this.canvasData;
return this.moveTo(
@@ -191,7 +183,7 @@ export default {
* Move the canvas to an absolute point
* @param {number} x - The x-axis coordinate.
* @param {number} [y=x] - The y-axis coordinate.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
moveTo(x, y = x) {
const { canvasData } = this;
@@ -223,7 +215,7 @@ export default {
* Zoom the canvas with a relative ratio
* @param {number} ratio - The target ratio.
* @param {Event} _originalEvent - The original event if any.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
zoom(ratio, _originalEvent) {
const { canvasData } = this;
@@ -244,7 +236,7 @@ export default {
* @param {number} ratio - The target ratio.
* @param {Object} pivot - The zoom pivot point coordinate.
* @param {Event} _originalEvent - The original event if any.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
zoomTo(ratio, pivot, _originalEvent) {
const { options, canvasData } = this;
@@ -262,9 +254,9 @@ export default {
const newHeight = naturalHeight * ratio;
if (dispatchEvent(this.element, EVENT_ZOOM, {
- originalEvent: _originalEvent,
+ ratio,
oldRatio: width / naturalWidth,
- ratio: newWidth / naturalWidth,
+ originalEvent: _originalEvent,
}) === false) {
return this;
}
@@ -308,7 +300,7 @@ export default {
/**
* Rotate the canvas with a relative degree
* @param {number} degree - The rotate degree.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
rotate(degree) {
return this.rotateTo((this.imageData.rotate || 0) + Number(degree));
@@ -317,7 +309,7 @@ export default {
/**
* Rotate the canvas to an absolute degree
* @param {number} degree - The rotate degree.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
rotateTo(degree) {
degree = Number(degree);
@@ -333,7 +325,7 @@ export default {
/**
* Scale the image on the x-axis.
* @param {number} scaleX - The scale ratio on the x-axis.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
scaleX(scaleX) {
const { scaleY } = this.imageData;
@@ -344,7 +336,7 @@ export default {
/**
* Scale the image on the y-axis.
* @param {number} scaleY - The scale ratio on the y-axis.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
scaleY(scaleY) {
const { scaleX } = this.imageData;
@@ -356,7 +348,7 @@ export default {
* Scale the image
* @param {number} scaleX - The scale ratio on the x-axis.
* @param {number} [scaleY=scaleX] - The scale ratio on the y-axis.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
scale(scaleX, scaleY = scaleX) {
const { imageData } = this;
@@ -408,10 +400,21 @@ export default {
const ratio = imageData.width / imageData.naturalWidth;
- each(data, (n, i) => {
- n /= ratio;
- data[i] = rounded ? Math.round(n) : n;
+ forEach(data, (n, i) => {
+ data[i] = n / ratio;
});
+
+ if (rounded) {
+ // In case rounding off leads to extra 1px in right or bottom border
+ // we should round the top-left corner and the dimension (#343).
+ const bottom = Math.round(data.y + data.height);
+ const right = Math.round(data.x + data.width);
+
+ data.x = Math.round(data.x);
+ data.y = Math.round(data.y);
+ data.width = right - data.x;
+ data.height = bottom - data.y;
+ }
} else {
data = {
x: 0,
@@ -436,16 +439,12 @@ export default {
/**
* Set the cropped area position and size with new data
* @param {Object} data - The new data.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
setData(data) {
const { options, imageData, canvasData } = this;
const cropBoxData = {};
- if (isFunction(data)) {
- data = data.call(this.element);
- }
-
if (this.ready && !this.disabled && isPlainObject(data)) {
let transformed = false;
@@ -501,7 +500,7 @@ export default {
* @returns {Object} The result container data.
*/
getContainerData() {
- return this.ready ? extend({}, this.containerData) : {};
+ return this.ready ? assign({}, this.containerData) : {};
},
/**
@@ -509,7 +508,7 @@ export default {
* @returns {Object} The result image data.
*/
getImageData() {
- return this.loaded ? extend({}, this.imageData) : {};
+ return this.sized ? assign({}, this.imageData) : {};
},
/**
@@ -521,7 +520,7 @@ export default {
const data = {};
if (this.ready) {
- each([
+ forEach([
'left',
'top',
'width',
@@ -539,16 +538,12 @@ export default {
/**
* Set the canvas position and size with new data.
* @param {Object} data - The new canvas data.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
setCanvasData(data) {
const { canvasData } = this;
const { aspectRatio } = canvasData;
- if (isFunction(data)) {
- data = data.call(this.element);
- }
-
if (this.ready && !this.disabled && isPlainObject(data)) {
if (isNumber(data.left)) {
canvasData.left = data.left;
@@ -595,7 +590,7 @@ export default {
/**
* Set the crop box position and size with new data.
* @param {Object} data - The new crop box data.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
setCropBoxData(data) {
const { cropBoxData } = this;
@@ -603,10 +598,6 @@ export default {
let widthChanged;
let heightChanged;
- if (isFunction(data)) {
- data = data.call(this.element);
- }
-
if (this.ready && this.cropped && !this.disabled && isPlainObject(data)) {
if (isNumber(data.left)) {
cropBoxData.left = data.left;
@@ -761,8 +752,6 @@ export default {
dstHeight = srcHeight;
}
- // All the numerical parameters should be integer for `drawImage`
- // https://github.com/fengyuanchen/cropper/issues/476
const params = [
srcX,
srcY,
@@ -782,7 +771,9 @@ export default {
);
}
- context.drawImage(source, ...params.map(param => Math.floor(normalizeDecimalNumber(param))));
+ // All the numerical parameters should be integer for `drawImage`
+ // https://github.com/fengyuanchen/cropper/issues/476
+ context.drawImage(source, ...params.map((param) => Math.floor(normalizeDecimalNumber(param))));
return canvas;
},
@@ -790,7 +781,7 @@ export default {
/**
* Change the aspect ratio of the crop box.
* @param {number} aspectRatio - The new aspect ratio.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
setAspectRatio(aspectRatio) {
const { options } = this;
@@ -814,17 +805,18 @@ export default {
/**
* Change the drag mode.
* @param {string} mode - The new drag mode.
- * @returns {Object} this
+ * @returns {Cropper} this
*/
setDragMode(mode) {
const { options, dragBox, face } = this;
- if (this.loaded && !this.disabled) {
+ if (this.ready && !this.disabled) {
const croppable = mode === DRAG_MODE_CROP;
const movable = options.movable && mode === DRAG_MODE_MOVE;
mode = (croppable || movable) ? mode : DRAG_MODE_NONE;
+ options.dragMode = mode;
setData(dragBox, DATA_ACTION, mode);
toggleClass(dragBox, CLASS_CROP, croppable);
toggleClass(dragBox, CLASS_MOVE, movable);
diff --git a/library/cropperjs/src/js/preview.js b/library/cropperjs/src/js/preview.js
index c47d03f4b..b73ac391c 100644
--- a/library/cropperjs/src/js/preview.js
+++ b/library/cropperjs/src/js/preview.js
@@ -1,10 +1,7 @@
+import { DATA_PREVIEW } from './constants';
import {
- DATA_PREVIEW,
-} from './constants';
-import {
- each,
- empty,
- extend,
+ assign,
+ forEach,
getData,
getTransforms,
removeData,
@@ -14,9 +11,10 @@ import {
export default {
initPreview() {
- const { crossOrigin } = this;
+ const { element, crossOrigin } = this;
const { preview } = this.options;
const url = crossOrigin ? this.crossOriginUrl : this.url;
+ const alt = element.alt || 'The image to preview';
const image = document.createElement('img');
if (crossOrigin) {
@@ -24,25 +22,32 @@ export default {
}
image.src = url;
+ image.alt = alt;
this.viewBox.appendChild(image);
- this.image2 = image;
+ this.viewBoxImage = image;
if (!preview) {
return;
}
- const previews = preview.querySelector ? [preview] : document.querySelectorAll(preview);
+ let previews = preview;
+
+ if (typeof preview === 'string') {
+ previews = element.ownerDocument.querySelectorAll(preview);
+ } else if (preview.querySelector) {
+ previews = [preview];
+ }
this.previews = previews;
- each(previews, (element) => {
+ forEach(previews, (el) => {
const img = document.createElement('img');
// Save the original size for recover
- setData(element, DATA_PREVIEW, {
- width: element.offsetWidth,
- height: element.offsetHeight,
- html: element.innerHTML,
+ setData(el, DATA_PREVIEW, {
+ width: el.offsetWidth,
+ height: el.offsetHeight,
+ html: el.innerHTML,
});
if (crossOrigin) {
@@ -50,6 +55,7 @@ export default {
}
img.src = url;
+ img.alt = alt;
/**
* Override img element styles
@@ -58,23 +64,23 @@ export default {
* (Occur only when margin-top <= -height)
*/
img.style.cssText = (
- 'display:block;' +
- 'width:100%;' +
- 'height:auto;' +
- 'min-width:0!important;' +
- 'min-height:0!important;' +
- 'max-width:none!important;' +
- 'max-height:none!important;' +
- 'image-orientation:0deg!important;"'
+ 'display:block;'
+ + 'width:100%;'
+ + 'height:auto;'
+ + 'min-width:0!important;'
+ + 'min-height:0!important;'
+ + 'max-width:none!important;'
+ + 'max-height:none!important;'
+ + 'image-orientation:0deg!important;"'
);
- empty(element);
- element.appendChild(img);
+ el.innerHTML = '';
+ el.appendChild(img);
});
},
resetPreview() {
- each(this.previews, (element) => {
+ forEach(this.previews, (element) => {
const data = getData(element, DATA_PREVIEW);
setStyle(element, {
@@ -98,15 +104,15 @@ export default {
return;
}
- setStyle(this.image2, extend({
+ setStyle(this.viewBoxImage, assign({
width,
height,
- }, getTransforms(extend({
+ }, getTransforms(assign({
translateX: -left,
translateY: -top,
}, imageData))));
- each(this.previews, (element) => {
+ forEach(this.previews, (element) => {
const data = getData(element, DATA_PREVIEW);
const originalWidth = data.width;
const originalHeight = data.height;
@@ -130,10 +136,10 @@ export default {
height: newHeight,
});
- setStyle(element.getElementsByTagName('img')[0], extend({
+ setStyle(element.getElementsByTagName('img')[0], assign({
width: width * ratio,
height: height * ratio,
- }, getTransforms(extend({
+ }, getTransforms(assign({
translateX: -left * ratio,
translateY: -top * ratio,
}, imageData))));
diff --git a/library/cropperjs/src/js/render.js b/library/cropperjs/src/js/render.js
index 09085ce41..ab8262d33 100644
--- a/library/cropperjs/src/js/render.js
+++ b/library/cropperjs/src/js/render.js
@@ -7,8 +7,8 @@ import {
} from './constants';
import {
addClass,
+ assign,
dispatchEvent,
- extend,
getAdjustedSizes,
getRotatedSizes,
getTransforms,
@@ -101,8 +101,8 @@ export default {
this.canvasData = canvasData;
this.limited = (viewMode === 1 || viewMode === 2);
this.limitCanvas(true, true);
- this.initialImageData = extend({}, imageData);
- this.initialCanvasData = extend({}, canvasData);
+ this.initialImageData = assign({}, imageData);
+ this.initialCanvasData = assign({}, canvasData);
},
limitCanvas(sizeLimited, positionLimited) {
@@ -158,7 +158,7 @@ export default {
aspectRatio,
width: minCanvasWidth,
height: minCanvasHeight,
- }, 'cover'));
+ }));
canvasData.minWidth = minCanvasWidth;
canvasData.minHeight = minCanvasHeight;
@@ -167,7 +167,7 @@ export default {
}
if (positionLimited) {
- if (viewMode) {
+ if (viewMode > (cropped ? 0 : 1)) {
const newCanvasLeft = containerData.width - canvasData.width;
const newCanvasTop = containerData.height - canvasData.height;
@@ -231,13 +231,13 @@ export default {
this.limitCanvas(true, false);
}
- if (canvasData.width > canvasData.maxWidth ||
- canvasData.width < canvasData.minWidth) {
+ if (canvasData.width > canvasData.maxWidth
+ || canvasData.width < canvasData.minWidth) {
canvasData.left = canvasData.oldLeft;
}
- if (canvasData.height > canvasData.maxHeight ||
- canvasData.height < canvasData.minHeight) {
+ if (canvasData.height > canvasData.maxHeight
+ || canvasData.height < canvasData.minHeight) {
canvasData.top = canvasData.oldTop;
}
@@ -263,7 +263,7 @@ export default {
canvasData.oldLeft = canvasData.left;
canvasData.oldTop = canvasData.top;
- setStyle(this.canvas, extend({
+ setStyle(this.canvas, assign({
width: canvasData.width,
height: canvasData.height,
}, getTransforms({
@@ -283,16 +283,16 @@ export default {
const width = imageData.naturalWidth * (canvasData.width / canvasData.naturalWidth);
const height = imageData.naturalHeight * (canvasData.height / canvasData.naturalHeight);
- extend(imageData, {
+ assign(imageData, {
width,
height,
left: (canvasData.width - width) / 2,
top: (canvasData.height - height) / 2,
});
- setStyle(this.image, extend({
+ setStyle(this.image, assign({
width: imageData.width,
height: imageData.height,
- }, getTransforms(extend({
+ }, getTransforms(assign({
translateX: imageData.left,
translateY: imageData.top,
}, imageData))));
@@ -304,7 +304,7 @@ export default {
initCropBox() {
const { options, canvasData } = this;
- const { aspectRatio } = options;
+ const aspectRatio = options.aspectRatio || options.initialAspectRatio;
const autoCropArea = Number(options.autoCropArea) || 0.8;
const cropBoxData = {
width: canvasData.width,
@@ -350,7 +350,7 @@ export default {
cropBoxData.oldLeft = cropBoxData.left;
cropBoxData.oldTop = cropBoxData.top;
- this.initialCropBoxData = extend({}, cropBoxData);
+ this.initialCropBoxData = assign({}, cropBoxData);
},
limitCropBox(sizeLimited, positionLimited) {
@@ -366,14 +366,18 @@ export default {
if (sizeLimited) {
let minCropBoxWidth = Number(options.minCropBoxWidth) || 0;
let minCropBoxHeight = Number(options.minCropBoxHeight) || 0;
- let maxCropBoxWidth = Math.min(
+ let maxCropBoxWidth = limited ? Math.min(
containerData.width,
- limited ? canvasData.width : containerData.width,
- );
- let maxCropBoxHeight = Math.min(
+ canvasData.width,
+ canvasData.width + canvasData.left,
+ containerData.width - canvasData.left,
+ ) : containerData.width;
+ let maxCropBoxHeight = limited ? Math.min(
containerData.height,
- limited ? canvasData.height : containerData.height,
- );
+ canvasData.height,
+ canvasData.height + canvasData.top,
+ containerData.height - canvasData.top,
+ ) : containerData.height;
// The min/maxCropBoxWidth/Height must be less than container's width/height
minCropBoxWidth = Math.min(minCropBoxWidth, containerData.width);
@@ -430,13 +434,13 @@ export default {
renderCropBox() {
const { options, containerData, cropBoxData } = this;
- if (cropBoxData.width > cropBoxData.maxWidth ||
- cropBoxData.width < cropBoxData.minWidth) {
+ if (cropBoxData.width > cropBoxData.maxWidth
+ || cropBoxData.width < cropBoxData.minWidth) {
cropBoxData.left = cropBoxData.oldLeft;
}
- if (cropBoxData.height > cropBoxData.maxHeight ||
- cropBoxData.height < cropBoxData.minHeight) {
+ if (cropBoxData.height > cropBoxData.maxHeight
+ || cropBoxData.height < cropBoxData.minHeight) {
cropBoxData.top = cropBoxData.oldTop;
}
@@ -464,11 +468,11 @@ export default {
if (options.movable && options.cropBoxMovable) {
// Turn to move the canvas when the crop box is equal to the container
- setData(this.face, DATA_ACTION, cropBoxData.width >= containerData.width &&
- cropBoxData.height >= containerData.height ? ACTION_MOVE : ACTION_ALL);
+ setData(this.face, DATA_ACTION, cropBoxData.width >= containerData.width
+ && cropBoxData.height >= containerData.height ? ACTION_MOVE : ACTION_ALL);
}
- setStyle(this.cropBox, extend({
+ setStyle(this.cropBox, assign({
width: cropBoxData.width,
height: cropBoxData.height,
}, getTransforms({
@@ -487,9 +491,6 @@ export default {
output() {
this.preview();
-
- if (this.complete) {
- dispatchEvent(this.element, EVENT_CROP, this.getData());
- }
+ dispatchEvent(this.element, EVENT_CROP, this.getData());
},
};
diff --git a/library/cropperjs/src/js/template.js b/library/cropperjs/src/js/template.js
index 589b46a78..e14ce750b 100644
--- a/library/cropperjs/src/js/template.js
+++ b/library/cropperjs/src/js/template.js
@@ -1,27 +1,27 @@
export default (
- '<div class="cropper-container">' +
- '<div class="cropper-wrap-box">' +
- '<div class="cropper-canvas"></div>' +
- '</div>' +
- '<div class="cropper-drag-box"></div>' +
- '<div class="cropper-crop-box">' +
- '<span class="cropper-view-box"></span>' +
- '<span class="cropper-dashed dashed-h"></span>' +
- '<span class="cropper-dashed dashed-v"></span>' +
- '<span class="cropper-center"></span>' +
- '<span class="cropper-face"></span>' +
- '<span class="cropper-line line-e" data-action="e"></span>' +
- '<span class="cropper-line line-n" data-action="n"></span>' +
- '<span class="cropper-line line-w" data-action="w"></span>' +
- '<span class="cropper-line line-s" data-action="s"></span>' +
- '<span class="cropper-point point-e" data-action="e"></span>' +
- '<span class="cropper-point point-n" data-action="n"></span>' +
- '<span class="cropper-point point-w" data-action="w"></span>' +
- '<span class="cropper-point point-s" data-action="s"></span>' +
- '<span class="cropper-point point-ne" data-action="ne"></span>' +
- '<span class="cropper-point point-nw" data-action="nw"></span>' +
- '<span class="cropper-point point-sw" data-action="sw"></span>' +
- '<span class="cropper-point point-se" data-action="se"></span>' +
- '</div>' +
- '</div>'
+ '<div class="cropper-container" touch-action="none">'
+ + '<div class="cropper-wrap-box">'
+ + '<div class="cropper-canvas"></div>'
+ + '</div>'
+ + '<div class="cropper-drag-box"></div>'
+ + '<div class="cropper-crop-box">'
+ + '<span class="cropper-view-box"></span>'
+ + '<span class="cropper-dashed dashed-h"></span>'
+ + '<span class="cropper-dashed dashed-v"></span>'
+ + '<span class="cropper-center"></span>'
+ + '<span class="cropper-face"></span>'
+ + '<span class="cropper-line line-e" data-cropper-action="e"></span>'
+ + '<span class="cropper-line line-n" data-cropper-action="n"></span>'
+ + '<span class="cropper-line line-w" data-cropper-action="w"></span>'
+ + '<span class="cropper-line line-s" data-cropper-action="s"></span>'
+ + '<span class="cropper-point point-e" data-cropper-action="e"></span>'
+ + '<span class="cropper-point point-n" data-cropper-action="n"></span>'
+ + '<span class="cropper-point point-w" data-cropper-action="w"></span>'
+ + '<span class="cropper-point point-s" data-cropper-action="s"></span>'
+ + '<span class="cropper-point point-ne" data-cropper-action="ne"></span>'
+ + '<span class="cropper-point point-nw" data-cropper-action="nw"></span>'
+ + '<span class="cropper-point point-sw" data-cropper-action="sw"></span>'
+ + '<span class="cropper-point point-se" data-cropper-action="se"></span>'
+ + '</div>'
+ + '</div>'
);
diff --git a/library/cropperjs/src/js/utilities.js b/library/cropperjs/src/js/utilities.js
index 50b586a76..bb5c7c24c 100644
--- a/library/cropperjs/src/js/utilities.js
+++ b/library/cropperjs/src/js/utilities.js
@@ -1,6 +1,4 @@
-import {
- WINDOW,
-} from './constants';
+import { IS_BROWSER, WINDOW } from './constants';
/**
* Check if the given value is not a number.
@@ -17,6 +15,13 @@ export function isNumber(value) {
}
/**
+ * Check if the given value is a positive number.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a positive number, else `false`.
+ */
+export const isPositiveNumber = (value) => value > 0 && value < Infinity;
+
+/**
* Check if the given value is undefined.
* @param {*} value - The value to check.
* @returns {boolean} Returns `true` if the given value is undefined, else `false`.
@@ -51,7 +56,7 @@ export function isPlainObject(value) {
const { prototype } = constructor;
return constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf');
- } catch (e) {
+ } catch (error) {
return false;
}
}
@@ -65,23 +70,29 @@ export function isFunction(value) {
return typeof value === 'function';
}
+const { slice } = Array.prototype;
+
+/**
+ * Convert array-like or iterable object to an array.
+ * @param {*} value - The value to convert.
+ * @returns {Array} Returns a new array.
+ */
+export function toArray(value) {
+ return Array.from ? Array.from(value) : slice.call(value);
+}
+
/**
* Iterate the given data.
* @param {*} data - The data to iterate.
* @param {Function} callback - The process function for each element.
* @returns {*} The original data.
*/
-export function each(data, callback) {
+export function forEach(data, callback) {
if (data && isFunction(callback)) {
if (Array.isArray(data) || isNumber(data.length)/* array-like */) {
- const { length } = data;
- let i;
-
- for (i = 0; i < length; i += 1) {
- if (callback.call(data, data[i], i, data) === false) {
- break;
- }
- }
+ toArray(data).forEach((value, key) => {
+ callback.call(data, value, key, data);
+ });
} else if (isObject(data)) {
Object.keys(data).forEach((key) => {
callback.call(data, data[key], key, data);
@@ -94,43 +105,29 @@ export function each(data, callback) {
/**
* Extend the given object.
- * @param {*} obj - The object to be extended.
- * @param {*} args - The rest objects which will be merged to the first object.
+ * @param {*} target - The target object to extend.
+ * @param {*} args - The rest objects for merging to the target object.
* @returns {Object} The extended object.
*/
-export function extend(obj, ...args) {
- if (isObject(obj) && args.length > 0) {
- if (Object.assign) {
- return Object.assign(obj, ...args);
- }
-
+export const assign = Object.assign || function assign(target, ...args) {
+ if (isObject(target) && args.length > 0) {
args.forEach((arg) => {
if (isObject(arg)) {
Object.keys(arg).forEach((key) => {
- obj[key] = arg[key];
+ target[key] = arg[key];
});
}
});
}
- return obj;
-}
-
-/**
- * Takes a function and returns a new one that will always have a particular context.
- * @param {Function} fn - The target function.
- * @param {Object} context - The new context for the function.
- * @returns {boolean} The new function.
- */
-export function proxy(fn, context, ...args) {
- return (...args2) => fn.apply(context, args.concat(args2));
-}
+ return target;
+};
-const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/i;
+const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
/**
* Normalize decimal number.
- * Check out {@link http://0.30000000000000004.com/ }
+ * Check out {@link http://0.30000000000000004.com/}
* @param {number} value - The value to normalize.
* @param {number} [times=100000000000] - The times for normalizing.
* @returns {number} Returns the normalized number.
@@ -139,7 +136,7 @@ export function normalizeDecimalNumber(value, times = 100000000000) {
return REGEXP_DECIMALS.test(value) ? (Math.round(value * times) / times) : value;
}
-const REGEXP_SUFFIX = /^(?:width|height|left|top|marginLeft|marginTop)$/;
+const REGEXP_SUFFIX = /^width|height|left|top|marginLeft|marginTop$/;
/**
* Apply styles to the given element.
@@ -149,9 +146,9 @@ const REGEXP_SUFFIX = /^(?:width|height|left|top|marginLeft|marginTop)$/;
export function setStyle(element, styles) {
const { style } = element;
- each(styles, (value, property) => {
+ forEach(styles, (value, property) => {
if (REGEXP_SUFFIX.test(property) && isNumber(value)) {
- value += 'px';
+ value = `${value}px`;
}
style[property] = value;
@@ -165,9 +162,9 @@ export function setStyle(element, styles) {
* @returns {boolean} Returns `true` if the special class was found.
*/
export function hasClass(element, value) {
- return element.classList ?
- element.classList.contains(value) :
- element.className.indexOf(value) > -1;
+ return element.classList
+ ? element.classList.contains(value)
+ : element.className.indexOf(value) > -1;
}
/**
@@ -181,7 +178,7 @@ export function addClass(element, value) {
}
if (isNumber(element.length)) {
- each(element, (elem) => {
+ forEach(element, (elem) => {
addClass(elem, value);
});
return;
@@ -212,7 +209,7 @@ export function removeClass(element, value) {
}
if (isNumber(element.length)) {
- each(element, (elem) => {
+ forEach(element, (elem) => {
removeClass(elem, value);
});
return;
@@ -240,7 +237,7 @@ export function toggleClass(element, value, added) {
}
if (isNumber(element.length)) {
- each(element, (elem) => {
+ forEach(element, (elem) => {
toggleClass(elem, value, added);
});
return;
@@ -254,15 +251,15 @@ export function toggleClass(element, value, added) {
}
}
-const REGEXP_HYPHENATE = /([a-z\d])([A-Z])/g;
+const REGEXP_CAMEL_CASE = /([a-z\d])([A-Z])/g;
/**
- * Hyphenate the given value.
- * @param {string} value - The value to hyphenate.
- * @returns {string} The hyphenated value.
+ * Transform the given string from camelCase to kebab-case
+ * @param {string} value - The value to transform.
+ * @returns {string} The transformed value.
*/
-export function hyphenate(value) {
- return value.replace(REGEXP_HYPHENATE, '$1-$2').toLowerCase();
+export function toParamCase(value) {
+ return value.replace(REGEXP_CAMEL_CASE, '$1-$2').toLowerCase();
}
/**
@@ -274,11 +271,13 @@ export function hyphenate(value) {
export function getData(element, name) {
if (isObject(element[name])) {
return element[name];
- } else if (element.dataset) {
+ }
+
+ if (element.dataset) {
return element.dataset[name];
}
- return element.getAttribute(`data-${hyphenate(name)}`);
+ return element.getAttribute(`data-${toParamCase(name)}`);
}
/**
@@ -293,7 +292,7 @@ export function setData(element, name, data) {
} else if (element.dataset) {
element.dataset[name] = data;
} else {
- element.setAttribute(`data-${hyphenate(name)}`, data);
+ element.setAttribute(`data-${toParamCase(name)}`, data);
}
}
@@ -306,22 +305,50 @@ export function removeData(element, name) {
if (isObject(element[name])) {
try {
delete element[name];
- } catch (e) {
- element[name] = null;
+ } catch (error) {
+ element[name] = undefined;
}
} else if (element.dataset) {
// #128 Safari not allows to delete dataset property
try {
delete element.dataset[name];
- } catch (e) {
- element.dataset[name] = null;
+ } catch (error) {
+ element.dataset[name] = undefined;
}
} else {
- element.removeAttribute(`data-${hyphenate(name)}`);
+ element.removeAttribute(`data-${toParamCase(name)}`);
}
}
const REGEXP_SPACES = /\s\s*/;
+const onceSupported = (() => {
+ let supported = false;
+
+ if (IS_BROWSER) {
+ let once = false;
+ const listener = () => {};
+ const options = Object.defineProperty({}, 'once', {
+ get() {
+ supported = true;
+ return once;
+ },
+
+ /**
+ * This setter can fix a `TypeError` in strict mode
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only}
+ * @param {boolean} value - The value to set
+ */
+ set(value) {
+ once = value;
+ },
+ });
+
+ WINDOW.addEventListener('test', listener, options);
+ WINDOW.removeEventListener('test', listener, options);
+ }
+
+ return supported;
+})();
/**
* Remove event listener from the target element.
@@ -331,24 +358,28 @@ const REGEXP_SPACES = /\s\s*/;
* @param {Object} options - The event options.
*/
export function removeListener(element, type, listener, options = {}) {
- if (!isFunction(listener)) {
- return;
- }
+ let handler = listener;
- const types = type.trim().split(REGEXP_SPACES);
+ type.trim().split(REGEXP_SPACES).forEach((event) => {
+ if (!onceSupported) {
+ const { listeners } = element;
- if (types.length > 1) {
- each(types, (t) => {
- removeListener(element, t, listener, options);
- });
- return;
- }
+ if (listeners && listeners[event] && listeners[event][listener]) {
+ handler = listeners[event][listener];
+ delete listeners[event][listener];
- if (element.removeEventListener) {
- element.removeEventListener(type, listener, options);
- } else if (element.detachEvent) {
- element.detachEvent(`on${type}`, listener);
- }
+ if (Object.keys(listeners[event]).length === 0) {
+ delete listeners[event];
+ }
+
+ if (Object.keys(listeners).length === 0) {
+ delete element.listeners;
+ }
+ }
+ }
+
+ element.removeEventListener(event, handler, options);
+ });
}
/**
@@ -359,33 +390,32 @@ export function removeListener(element, type, listener, options = {}) {
* @param {Object} options - The event options.
*/
export function addListener(element, type, listener, options = {}) {
- if (!isFunction(listener)) {
- return;
- }
+ let handler = listener;
- const types = type.trim().split(REGEXP_SPACES);
+ type.trim().split(REGEXP_SPACES).forEach((event) => {
+ if (options.once && !onceSupported) {
+ const { listeners = {} } = element;
- if (types.length > 1) {
- each(types, (t) => {
- addListener(element, t, listener, options);
- });
- return;
- }
+ handler = (...args) => {
+ delete listeners[event][listener];
+ element.removeEventListener(event, handler, options);
+ listener.apply(element, args);
+ };
- if (options.once) {
- const originalListener = listener;
+ if (!listeners[event]) {
+ listeners[event] = {};
+ }
- listener = (...args) => {
- removeListener(element, type, listener, options);
- return originalListener.apply(element, args);
- };
- }
+ if (listeners[event][listener]) {
+ element.removeEventListener(event, listeners[event][listener], options);
+ }
- if (element.addEventListener) {
- element.addEventListener(type, listener, options);
- } else if (element.attachEvent) {
- element.attachEvent(`on${type}`, listener);
- }
+ listeners[event][listener] = handler;
+ element.listeners = listeners;
+ }
+
+ element.addEventListener(event, handler, options);
+ });
}
/**
@@ -396,39 +426,21 @@ export function addListener(element, type, listener, options = {}) {
* @returns {boolean} Indicate if the event is default prevented or not.
*/
export function dispatchEvent(element, type, data) {
- if (element.dispatchEvent) {
- let event;
-
- // Event and CustomEvent on IE9-11 are global objects, not constructors
- if (isFunction(Event) && isFunction(CustomEvent)) {
- if (isUndefined(data)) {
- event = new Event(type, {
- bubbles: true,
- cancelable: true,
- });
- } else {
- event = new CustomEvent(type, {
- detail: data,
- bubbles: true,
- cancelable: true,
- });
- }
- } else if (isUndefined(data)) {
- event = document.createEvent('Event');
- event.initEvent(type, true, true);
- } else {
- event = document.createEvent('CustomEvent');
- event.initCustomEvent(type, true, true, data);
- }
-
- // IE9+
- return element.dispatchEvent(event);
- } else if (element.fireEvent) {
- // IE6-10 (native events only)
- return element.fireEvent(`on${type}`);
+ let event;
+
+ // Event and CustomEvent on IE9-11 are global objects, not constructors
+ if (isFunction(Event) && isFunction(CustomEvent)) {
+ event = new CustomEvent(type, {
+ detail: data,
+ bubbles: true,
+ cancelable: true,
+ });
+ } else {
+ event = document.createEvent('CustomEvent');
+ event.initCustomEvent(type, true, true, data);
}
- return true;
+ return element.dispatchEvent(event);
}
/**
@@ -437,31 +449,16 @@ export function dispatchEvent(element, type, data) {
* @returns {Object} The offset data.
*/
export function getOffset(element) {
- const doc = document.documentElement;
const box = element.getBoundingClientRect();
return {
- left: box.left + (
- (window.scrollX || (doc && doc.scrollLeft) || 0) - ((doc && doc.clientLeft) || 0)
- ),
- top: box.top + (
- (window.scrollY || (doc && doc.scrollTop) || 0) - ((doc && doc.clientTop) || 0)
- ),
+ left: box.left + (window.pageXOffset - document.documentElement.clientLeft),
+ top: box.top + (window.pageYOffset - document.documentElement.clientTop),
};
}
-/**
- * Empty an element.
- * @param {Element} element - The element to empty.
- */
-export function empty(element) {
- while (element.firstChild) {
- element.removeChild(element.firstChild);
- }
-}
-
const { location } = WINDOW;
-const REGEXP_ORIGINS = /^(https?:)\/\/([^:/?#]+):?(\d*)/i;
+const REGEXP_ORIGINS = /^(\w+:)\/\/([^:/?#]*):?(\d*)/i;
/**
* Check if the given URL is a cross origin URL.
@@ -471,10 +468,10 @@ const REGEXP_ORIGINS = /^(https?:)\/\/([^:/?#]+):?(\d*)/i;
export function isCrossOriginURL(url) {
const parts = url.match(REGEXP_ORIGINS);
- return parts && (
- parts[1] !== location.protocol ||
- parts[2] !== location.hostname ||
- parts[3] !== location.port
+ return parts !== null && (
+ parts[1] !== location.protocol
+ || parts[2] !== location.hostname
+ || parts[3] !== location.port
);
}
@@ -486,7 +483,7 @@ export function isCrossOriginURL(url) {
export function addTimestamp(url) {
const timestamp = `timestamp=${(new Date()).getTime()}`;
- return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
+ return url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp;
}
/**
@@ -533,65 +530,19 @@ export function getTransforms({
};
}
-const { navigator } = WINDOW;
-const IS_SAFARI_OR_UIWEBVIEW = navigator && /(Macintosh|iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent);
-
-/**
- * Get an image's natural sizes.
- * @param {string} image - The target image.
- * @param {Function} callback - The callback function.
- */
-export function getImageNaturalSizes(image, callback) {
- // Modern browsers (except Safari)
- if (image.naturalWidth && !IS_SAFARI_OR_UIWEBVIEW) {
- callback(image.naturalWidth, image.naturalHeight);
- return;
- }
-
- const newImage = document.createElement('img');
- const body = document.body || document.documentElement;
-
- newImage.onload = () => {
- callback(newImage.width, newImage.height);
-
- if (!IS_SAFARI_OR_UIWEBVIEW) {
- body.removeChild(newImage);
- }
- };
-
- newImage.src = image.src;
-
- // iOS Safari will convert the image automatically
- // with its orientation once append it into DOM (#279)
- if (!IS_SAFARI_OR_UIWEBVIEW) {
- newImage.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(newImage);
- }
-}
-
/**
* Get the max ratio of a group of pointers.
* @param {string} pointers - The target pointers.
* @returns {number} The result ratio.
*/
export function getMaxZoomRatio(pointers) {
- const pointers2 = extend({}, pointers);
+ const pointers2 = { ...pointers };
const ratios = [];
- each(pointers, (pointer, pointerId) => {
+ forEach(pointers, (pointer, pointerId) => {
delete pointers2[pointerId];
- each(pointers2, (pointer2) => {
+ forEach(pointers2, (pointer2) => {
const x1 = Math.abs(pointer.startX - pointer2.startX);
const y1 = Math.abs(pointer.startY - pointer2.startY);
const x2 = Math.abs(pointer.endX - pointer2.endX);
@@ -621,14 +572,11 @@ export function getPointer({ pageX, pageY }, endOnly) {
endY: pageY,
};
- if (endOnly) {
- return end;
- }
-
- return extend({
+ return endOnly ? end : ({
startX: pageX,
startY: pageY,
- }, end);
+ ...end,
+ });
}
/**
@@ -641,7 +589,7 @@ export function getPointersCenter(pointers) {
let pageY = 0;
let count = 0;
- each(pointers, ({ startX, startY }) => {
+ forEach(pointers, ({ startX, startY }) => {
pageX += startX;
pageY += startY;
count += 1;
@@ -657,11 +605,6 @@ export function getPointersCenter(pointers) {
}
/**
- * Check if the given value is a finite number.
- */
-export const isFinite = Number.isFinite || WINDOW.isFinite;
-
-/**
* Get the max sizes in a rectangle under the given aspect ratio.
* @param {Object} data - The original sizes.
* @param {string} [type='contain'] - The adjust type.
@@ -675,9 +618,10 @@ export function getAdjustedSizes(
},
type = 'contain', // or 'cover'
) {
- const isValidNumber = value => isFinite(value) && value > 0;
+ const isValidWidth = isPositiveNumber(width);
+ const isValidHeight = isPositiveNumber(height);
- if (isValidNumber(width) && isValidNumber(height)) {
+ if (isValidWidth && isValidHeight) {
const adjustedWidth = height * aspectRatio;
if ((type === 'contain' && adjustedWidth > width) || (type === 'cover' && adjustedWidth < width)) {
@@ -685,9 +629,9 @@ export function getAdjustedSizes(
} else {
width = height * aspectRatio;
}
- } else if (isValidNumber(width)) {
+ } else if (isValidWidth) {
height = width / aspectRatio;
- } else if (isValidNumber(height)) {
+ } else if (isValidHeight) {
width = height * aspectRatio;
}
@@ -738,6 +682,9 @@ export function getRotatedSizes({ width, height, degree }) {
export function getSourceCanvas(
image,
{
+ aspectRatio: imageAspectRatio,
+ naturalWidth: imageNaturalWidth,
+ naturalHeight: imageNaturalHeight,
rotate = 0,
scaleX = 1,
scaleY = 1,
@@ -771,11 +718,32 @@ export function getSourceCanvas(
}, 'cover');
const width = Math.min(maxSizes.width, Math.max(minSizes.width, naturalWidth));
const height = Math.min(maxSizes.height, Math.max(minSizes.height, naturalHeight));
+
+ // Note: should always use image's natural sizes for drawing as
+ // imageData.naturalWidth === canvasData.naturalHeight when rotate % 180 === 90
+ const destMaxSizes = getAdjustedSizes({
+ aspectRatio: imageAspectRatio,
+ width: maxWidth,
+ height: maxHeight,
+ });
+ const destMinSizes = getAdjustedSizes({
+ aspectRatio: imageAspectRatio,
+ width: minWidth,
+ height: minHeight,
+ }, 'cover');
+ const destWidth = Math.min(
+ destMaxSizes.width,
+ Math.max(destMinSizes.width, imageNaturalWidth),
+ );
+ const destHeight = Math.min(
+ destMaxSizes.height,
+ Math.max(destMinSizes.height, imageNaturalHeight),
+ );
const params = [
- -width / 2,
- -height / 2,
- width,
- height,
+ -destWidth / 2,
+ -destHeight / 2,
+ destWidth,
+ destHeight,
];
canvas.width = normalizeDecimalNumber(width);
@@ -788,7 +756,7 @@ export function getSourceCanvas(
context.scale(scaleX, scaleY);
context.imageSmoothingEnabled = imageSmoothingEnabled;
context.imageSmoothingQuality = imageSmoothingQuality;
- context.drawImage(image, ...params.map(param => Math.floor(normalizeDecimalNumber(param))));
+ context.drawImage(image, ...params.map((param) => Math.floor(normalizeDecimalNumber(param))));
context.restore();
return canvas;
}
@@ -804,11 +772,10 @@ const { fromCharCode } = String;
*/
export function getStringFromCharCode(dataView, start, length) {
let str = '';
- let i;
length += start;
- for (i = start; i < length; i += 1) {
+ for (let i = start; i < length; i += 1) {
str += fromCharCode(dataView.getUint8(i));
}
@@ -828,7 +795,7 @@ export function dataURLToArrayBuffer(dataURL) {
const arrayBuffer = new ArrayBuffer(binary.length);
const uint8 = new Uint8Array(arrayBuffer);
- each(uint8, (value, i) => {
+ forEach(uint8, (value, i) => {
uint8[i] = binary.charCodeAt(i);
});
@@ -842,15 +809,20 @@ export function dataURLToArrayBuffer(dataURL) {
* @returns {string} The result Data URL.
*/
export function arrayBufferToDataURL(arrayBuffer, mimeType) {
- const uint8 = new Uint8Array(arrayBuffer);
- let data = '';
+ const chunks = [];
- // TypedArray.prototype.forEach is not supported in some browsers.
- each(uint8, (value) => {
- data += fromCharCode(value);
- });
+ // Chunk Typed Array for better performance (#435)
+ const chunkSize = 8192;
+ let uint8 = new Uint8Array(arrayBuffer);
- return `data:${mimeType};base64,${btoa(data)}`;
+ while (uint8.length > 0) {
+ // XXX: Babel's `toConsumableArray` helper will throw error in IE or Safari 9
+ // eslint-disable-next-line prefer-spread
+ chunks.push(fromCharCode.apply(null, toArray(uint8.subarray(0, chunkSize))));
+ uint8 = uint8.subarray(chunkSize);
+ }
+
+ return `data:${mimeType};base64,${btoa(chunks.join(''))}`;
}
/**
@@ -858,69 +830,75 @@ export function arrayBufferToDataURL(arrayBuffer, mimeType) {
* @param {ArrayBuffer} arrayBuffer - The array buffer to read.
* @returns {number} The read orientation value.
*/
-export function getOrientation(arrayBuffer) {
+export function resetAndGetOrientation(arrayBuffer) {
const dataView = new DataView(arrayBuffer);
let orientation;
- let littleEndian;
- let app1Start;
- let ifdStart;
-
- // Only handle JPEG image (start by 0xFFD8)
- if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
- const length = dataView.byteLength;
- let offset = 2;
-
- while (offset < length) {
- if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
- app1Start = offset;
- break;
- }
- offset += 1;
+ // Ignores range error when the image does not have correct Exif information
+ try {
+ let littleEndian;
+ let app1Start;
+ let ifdStart;
+
+ // Only handle JPEG image (start by 0xFFD8)
+ if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
+ const length = dataView.byteLength;
+ let offset = 2;
+
+ while (offset + 1 < length) {
+ if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
+ app1Start = offset;
+ break;
+ }
+
+ offset += 1;
+ }
}
- }
- if (app1Start) {
- const exifIDCode = app1Start + 4;
- const tiffOffset = app1Start + 10;
+ if (app1Start) {
+ const exifIDCode = app1Start + 4;
+ const tiffOffset = app1Start + 10;
- if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
- const endianness = dataView.getUint16(tiffOffset);
+ if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
+ const endianness = dataView.getUint16(tiffOffset);
- littleEndian = endianness === 0x4949;
+ littleEndian = endianness === 0x4949;
- if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
- if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
- const firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
+ if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
+ if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
+ const firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
- if (firstIFDOffset >= 0x00000008) {
- ifdStart = tiffOffset + firstIFDOffset;
+ if (firstIFDOffset >= 0x00000008) {
+ ifdStart = tiffOffset + firstIFDOffset;
+ }
}
}
}
}
- }
- if (ifdStart) {
- const length = dataView.getUint16(ifdStart, littleEndian);
- let offset;
- let i;
+ if (ifdStart) {
+ const length = dataView.getUint16(ifdStart, littleEndian);
+ let offset;
+ let i;
- for (i = 0; i < length; i += 1) {
- offset = ifdStart + (i * 12) + 2;
+ for (i = 0; i < length; i += 1) {
+ offset = ifdStart + (i * 12) + 2;
- if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
- // 8 is the offset of the current tag's value
- offset += 8;
+ if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
+ // 8 is the offset of the current tag's value
+ offset += 8;
- // Get the original orientation value
- orientation = dataView.getUint16(offset, littleEndian);
+ // Get the original orientation value
+ orientation = dataView.getUint16(offset, littleEndian);
- // Override the orientation with its default value
- dataView.setUint16(offset, 1, littleEndian);
- break;
+ // Override the orientation with its default value
+ dataView.setUint16(offset, 1, littleEndian);
+ break;
+ }
}
}
+ } catch (error) {
+ orientation = 1;
}
return orientation;