diff options
Diffstat (limited to 'library/cropperjs/src/js/utilities.js')
-rw-r--r-- | library/cropperjs/src/js/utilities.js | 524 |
1 files changed, 251 insertions, 273 deletions
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; |