diff options
Diffstat (limited to 'library/cropperjs/src')
-rw-r--r-- | library/cropperjs/src/css/cropper.css | 46 | ||||
-rw-r--r-- | library/cropperjs/src/css/cropper.scss | 286 | ||||
-rw-r--r-- | library/cropperjs/src/index.css | 1 | ||||
-rw-r--r-- | library/cropperjs/src/index.js | 3 | ||||
-rw-r--r-- | library/cropperjs/src/index.scss | 1 | ||||
-rw-r--r-- | library/cropperjs/src/js/change.js | 128 | ||||
-rw-r--r-- | library/cropperjs/src/js/constants.js | 34 | ||||
-rw-r--r-- | library/cropperjs/src/js/cropper.js | 275 | ||||
-rw-r--r-- | library/cropperjs/src/js/defaults.js | 7 | ||||
-rw-r--r-- | library/cropperjs/src/js/events.js | 21 | ||||
-rw-r--r-- | library/cropperjs/src/js/handlers.js | 100 | ||||
-rw-r--r-- | library/cropperjs/src/js/methods.js | 156 | ||||
-rw-r--r-- | library/cropperjs/src/js/preview.js | 66 | ||||
-rw-r--r-- | library/cropperjs/src/js/render.js | 65 | ||||
-rw-r--r-- | library/cropperjs/src/js/template.js | 50 | ||||
-rw-r--r-- | library/cropperjs/src/js/utilities.js | 524 |
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; |