diff options
Diffstat (limited to 'library/cropperjs/src/js/utilities.js')
-rw-r--r-- | library/cropperjs/src/js/utilities.js | 985 |
1 files changed, 985 insertions, 0 deletions
diff --git a/library/cropperjs/src/js/utilities.js b/library/cropperjs/src/js/utilities.js new file mode 100644 index 000000000..50b586a76 --- /dev/null +++ b/library/cropperjs/src/js/utilities.js @@ -0,0 +1,985 @@ +import { + WINDOW, +} from './constants'; + +/** + * Check if the given value is not a number. + */ +export const isNaN = Number.isNaN || WINDOW.isNaN; + +/** + * Check if the given value is a number. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a number, else `false`. + */ +export function isNumber(value) { + return typeof value === 'number' && !isNaN(value); +} + +/** + * 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`. + */ +export function isUndefined(value) { + return typeof value === 'undefined'; +} + +/** + * Check if the given value is an object. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is an object, else `false`. + */ +export function isObject(value) { + return typeof value === 'object' && value !== null; +} + +const { hasOwnProperty } = Object.prototype; + +/** + * Check if the given value is a plain object. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a plain object, else `false`. + */ +export function isPlainObject(value) { + if (!isObject(value)) { + return false; + } + + try { + const { constructor } = value; + const { prototype } = constructor; + + return constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf'); + } catch (e) { + return false; + } +} + +/** + * Check if the given value is a function. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a function, else `false`. + */ +export function isFunction(value) { + return typeof value === 'function'; +} + +/** + * 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) { + 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; + } + } + } else if (isObject(data)) { + Object.keys(data).forEach((key) => { + callback.call(data, data[key], key, data); + }); + } + } + + return data; +} + +/** + * Extend the given object. + * @param {*} obj - The object to be extended. + * @param {*} args - The rest objects which will be merged to the first 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); + } + + args.forEach((arg) => { + if (isObject(arg)) { + Object.keys(arg).forEach((key) => { + obj[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)); +} + +const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/i; + +/** + * Normalize decimal number. + * 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. + */ +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)$/; + +/** + * Apply styles to the given element. + * @param {Element} element - The target element. + * @param {Object} styles - The styles for applying. + */ +export function setStyle(element, styles) { + const { style } = element; + + each(styles, (value, property) => { + if (REGEXP_SUFFIX.test(property) && isNumber(value)) { + value += 'px'; + } + + style[property] = value; + }); +} + +/** + * Check if the given element has a special class. + * @param {Element} element - The element to check. + * @param {string} value - The class to search. + * @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; +} + +/** + * Add classes to the given element. + * @param {Element} element - The target element. + * @param {string} value - The classes to be added. + */ +export function addClass(element, value) { + if (!value) { + return; + } + + if (isNumber(element.length)) { + each(element, (elem) => { + addClass(elem, value); + }); + return; + } + + if (element.classList) { + element.classList.add(value); + return; + } + + const className = element.className.trim(); + + if (!className) { + element.className = value; + } else if (className.indexOf(value) < 0) { + element.className = `${className} ${value}`; + } +} + +/** + * Remove classes from the given element. + * @param {Element} element - The target element. + * @param {string} value - The classes to be removed. + */ +export function removeClass(element, value) { + if (!value) { + return; + } + + if (isNumber(element.length)) { + each(element, (elem) => { + removeClass(elem, value); + }); + return; + } + + if (element.classList) { + element.classList.remove(value); + return; + } + + if (element.className.indexOf(value) >= 0) { + element.className = element.className.replace(value, ''); + } +} + +/** + * Add or remove classes from the given element. + * @param {Element} element - The target element. + * @param {string} value - The classes to be toggled. + * @param {boolean} added - Add only. + */ +export function toggleClass(element, value, added) { + if (!value) { + return; + } + + if (isNumber(element.length)) { + each(element, (elem) => { + toggleClass(elem, value, added); + }); + return; + } + + // IE10-11 doesn't support the second parameter of `classList.toggle` + if (added) { + addClass(element, value); + } else { + removeClass(element, value); + } +} + +const REGEXP_HYPHENATE = /([a-z\d])([A-Z])/g; + +/** + * Hyphenate the given value. + * @param {string} value - The value to hyphenate. + * @returns {string} The hyphenated value. + */ +export function hyphenate(value) { + return value.replace(REGEXP_HYPHENATE, '$1-$2').toLowerCase(); +} + +/** + * Get data from the given element. + * @param {Element} element - The target element. + * @param {string} name - The data key to get. + * @returns {string} The data value. + */ +export function getData(element, name) { + if (isObject(element[name])) { + return element[name]; + } else if (element.dataset) { + return element.dataset[name]; + } + + return element.getAttribute(`data-${hyphenate(name)}`); +} + +/** + * Set data to the given element. + * @param {Element} element - The target element. + * @param {string} name - The data key to set. + * @param {string} data - The data value. + */ +export function setData(element, name, data) { + if (isObject(data)) { + element[name] = data; + } else if (element.dataset) { + element.dataset[name] = data; + } else { + element.setAttribute(`data-${hyphenate(name)}`, data); + } +} + +/** + * Remove data from the given element. + * @param {Element} element - The target element. + * @param {string} name - The data key to remove. + */ +export function removeData(element, name) { + if (isObject(element[name])) { + try { + delete element[name]; + } catch (e) { + element[name] = null; + } + } else if (element.dataset) { + // #128 Safari not allows to delete dataset property + try { + delete element.dataset[name]; + } catch (e) { + element.dataset[name] = null; + } + } else { + element.removeAttribute(`data-${hyphenate(name)}`); + } +} + +const REGEXP_SPACES = /\s\s*/; + +/** + * Remove event listener from the target element. + * @param {Element} element - The event target. + * @param {string} type - The event type(s). + * @param {Function} listener - The event listener. + * @param {Object} options - The event options. + */ +export function removeListener(element, type, listener, options = {}) { + if (!isFunction(listener)) { + return; + } + + const types = type.trim().split(REGEXP_SPACES); + + if (types.length > 1) { + each(types, (t) => { + removeListener(element, t, listener, options); + }); + return; + } + + if (element.removeEventListener) { + element.removeEventListener(type, listener, options); + } else if (element.detachEvent) { + element.detachEvent(`on${type}`, listener); + } +} + +/** + * Add event listener to the target element. + * @param {Element} element - The event target. + * @param {string} type - The event type(s). + * @param {Function} listener - The event listener. + * @param {Object} options - The event options. + */ +export function addListener(element, type, listener, options = {}) { + if (!isFunction(listener)) { + return; + } + + const types = type.trim().split(REGEXP_SPACES); + + if (types.length > 1) { + each(types, (t) => { + addListener(element, t, listener, options); + }); + return; + } + + if (options.once) { + const originalListener = listener; + + listener = (...args) => { + removeListener(element, type, listener, options); + return originalListener.apply(element, args); + }; + } + + if (element.addEventListener) { + element.addEventListener(type, listener, options); + } else if (element.attachEvent) { + element.attachEvent(`on${type}`, listener); + } +} + +/** + * Dispatch event on the target element. + * @param {Element} element - The event target. + * @param {string} type - The event type(s). + * @param {Object} data - The additional event data. + * @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}`); + } + + return true; +} + +/** + * Get the offset base on the document. + * @param {Element} element - The target element. + * @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) + ), + }; +} + +/** + * 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; + +/** + * Check if the given URL is a cross origin URL. + * @param {string} url - The target URL. + * @returns {boolean} Returns `true` if the given URL is a cross origin URL, else `false`. + */ +export function isCrossOriginURL(url) { + const parts = url.match(REGEXP_ORIGINS); + + return parts && ( + parts[1] !== location.protocol || + parts[2] !== location.hostname || + parts[3] !== location.port + ); +} + +/** + * Add timestamp to the given URL. + * @param {string} url - The target URL. + * @returns {string} The result URL. + */ +export function addTimestamp(url) { + const timestamp = `timestamp=${(new Date()).getTime()}`; + + return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp); +} + +/** + * Get transforms base on the given object. + * @param {Object} obj - The target object. + * @returns {string} A string contains transform values. + */ +export function getTransforms({ + rotate, + scaleX, + scaleY, + translateX, + translateY, +}) { + const values = []; + + if (isNumber(translateX) && translateX !== 0) { + values.push(`translateX(${translateX}px)`); + } + + if (isNumber(translateY) && translateY !== 0) { + values.push(`translateY(${translateY}px)`); + } + + // Rotate should come first before scale to match orientation transform + if (isNumber(rotate) && rotate !== 0) { + values.push(`rotate(${rotate}deg)`); + } + + if (isNumber(scaleX) && scaleX !== 1) { + values.push(`scaleX(${scaleX})`); + } + + if (isNumber(scaleY) && scaleY !== 1) { + values.push(`scaleY(${scaleY})`); + } + + const transform = values.length ? values.join(' ') : 'none'; + + return { + WebkitTransform: transform, + msTransform: transform, + transform, + }; +} + +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 ratios = []; + + each(pointers, (pointer, pointerId) => { + delete pointers2[pointerId]; + + each(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); + const y2 = Math.abs(pointer.endY - pointer2.endY); + const z1 = Math.sqrt((x1 * x1) + (y1 * y1)); + const z2 = Math.sqrt((x2 * x2) + (y2 * y2)); + const ratio = (z2 - z1) / z1; + + ratios.push(ratio); + }); + }); + + ratios.sort((a, b) => Math.abs(a) < Math.abs(b)); + + return ratios[0]; +} + +/** + * Get a pointer from an event object. + * @param {Object} event - The target event object. + * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not. + * @returns {Object} The result pointer contains start and/or end point coordinates. + */ +export function getPointer({ pageX, pageY }, endOnly) { + const end = { + endX: pageX, + endY: pageY, + }; + + if (endOnly) { + return end; + } + + return extend({ + startX: pageX, + startY: pageY, + }, end); +} + +/** + * Get the center point coordinate of a group of pointers. + * @param {Object} pointers - The target pointers. + * @returns {Object} The center point coordinate. + */ +export function getPointersCenter(pointers) { + let pageX = 0; + let pageY = 0; + let count = 0; + + each(pointers, ({ startX, startY }) => { + pageX += startX; + pageY += startY; + count += 1; + }); + + pageX /= count; + pageY /= count; + + return { + pageX, + pageY, + }; +} + +/** + * 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. + * @returns {Object} The result sizes. + */ +export function getAdjustedSizes( + { + aspectRatio, + height, + width, + }, + type = 'contain', // or 'cover' +) { + const isValidNumber = value => isFinite(value) && value > 0; + + if (isValidNumber(width) && isValidNumber(height)) { + const adjustedWidth = height * aspectRatio; + + if ((type === 'contain' && adjustedWidth > width) || (type === 'cover' && adjustedWidth < width)) { + height = width / aspectRatio; + } else { + width = height * aspectRatio; + } + } else if (isValidNumber(width)) { + height = width / aspectRatio; + } else if (isValidNumber(height)) { + width = height * aspectRatio; + } + + return { + width, + height, + }; +} + +/** + * Get the new sizes of a rectangle after rotated. + * @param {Object} data - The original sizes. + * @returns {Object} The result sizes. + */ +export function getRotatedSizes({ width, height, degree }) { + degree = Math.abs(degree) % 180; + + if (degree === 90) { + return { + width: height, + height: width, + }; + } + + const arc = ((degree % 90) * Math.PI) / 180; + const sinArc = Math.sin(arc); + const cosArc = Math.cos(arc); + const newWidth = (width * cosArc) + (height * sinArc); + const newHeight = (width * sinArc) + (height * cosArc); + + return degree > 90 ? { + width: newHeight, + height: newWidth, + } : { + width: newWidth, + height: newHeight, + }; +} + +/** + * Get a canvas which drew the given image. + * @param {HTMLImageElement} image - The image for drawing. + * @param {Object} imageData - The image data. + * @param {Object} canvasData - The canvas data. + * @param {Object} options - The options. + * @returns {HTMLCanvasElement} The result canvas. + */ +export function getSourceCanvas( + image, + { + rotate = 0, + scaleX = 1, + scaleY = 1, + }, + { + aspectRatio, + naturalWidth, + naturalHeight, + }, + { + fillColor = 'transparent', + imageSmoothingEnabled = true, + imageSmoothingQuality = 'low', + maxWidth = Infinity, + maxHeight = Infinity, + minWidth = 0, + minHeight = 0, + }, +) { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + const maxSizes = getAdjustedSizes({ + aspectRatio, + width: maxWidth, + height: maxHeight, + }); + const minSizes = getAdjustedSizes({ + aspectRatio, + width: minWidth, + height: minHeight, + }, 'cover'); + const width = Math.min(maxSizes.width, Math.max(minSizes.width, naturalWidth)); + const height = Math.min(maxSizes.height, Math.max(minSizes.height, naturalHeight)); + const params = [ + -width / 2, + -height / 2, + width, + height, + ]; + + canvas.width = normalizeDecimalNumber(width); + canvas.height = normalizeDecimalNumber(height); + context.fillStyle = fillColor; + context.fillRect(0, 0, width, height); + context.save(); + context.translate(width / 2, height / 2); + context.rotate((rotate * Math.PI) / 180); + context.scale(scaleX, scaleY); + context.imageSmoothingEnabled = imageSmoothingEnabled; + context.imageSmoothingQuality = imageSmoothingQuality; + context.drawImage(image, ...params.map(param => Math.floor(normalizeDecimalNumber(param)))); + context.restore(); + return canvas; +} + +const { fromCharCode } = String; + +/** + * Get string from char code in data view. + * @param {DataView} dataView - The data view for read. + * @param {number} start - The start index. + * @param {number} length - The read length. + * @returns {string} The read result. + */ +export function getStringFromCharCode(dataView, start, length) { + let str = ''; + let i; + + length += start; + + for (i = start; i < length; i += 1) { + str += fromCharCode(dataView.getUint8(i)); + } + + return str; +} + +const REGEXP_DATA_URL_HEAD = /^data:.*,/; + +/** + * Transform Data URL to array buffer. + * @param {string} dataURL - The Data URL to transform. + * @returns {ArrayBuffer} The result array buffer. + */ +export function dataURLToArrayBuffer(dataURL) { + const base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, ''); + const binary = atob(base64); + const arrayBuffer = new ArrayBuffer(binary.length); + const uint8 = new Uint8Array(arrayBuffer); + + each(uint8, (value, i) => { + uint8[i] = binary.charCodeAt(i); + }); + + return arrayBuffer; +} + +/** + * Transform array buffer to Data URL. + * @param {ArrayBuffer} arrayBuffer - The array buffer to transform. + * @param {string} mimeType - The mime type of the Data URL. + * @returns {string} The result Data URL. + */ +export function arrayBufferToDataURL(arrayBuffer, mimeType) { + const uint8 = new Uint8Array(arrayBuffer); + let data = ''; + + // TypedArray.prototype.forEach is not supported in some browsers. + each(uint8, (value) => { + data += fromCharCode(value); + }); + + return `data:${mimeType};base64,${btoa(data)}`; +} + +/** + * Get orientation value from given array buffer. + * @param {ArrayBuffer} arrayBuffer - The array buffer to read. + * @returns {number} The read orientation value. + */ +export function getOrientation(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; + } + } + + if (app1Start) { + const exifIDCode = app1Start + 4; + const tiffOffset = app1Start + 10; + + if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') { + const endianness = dataView.getUint16(tiffOffset); + + littleEndian = endianness === 0x4949; + + 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 (ifdStart) { + const length = dataView.getUint16(ifdStart, littleEndian); + let offset; + let i; + + 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; + + // Get the original orientation value + orientation = dataView.getUint16(offset, littleEndian); + + // Override the orientation with its default value + dataView.setUint16(offset, 1, littleEndian); + break; + } + } + } + + return orientation; +} + +/** + * Parse Exif Orientation value. + * @param {number} orientation - The orientation to parse. + * @returns {Object} The parsed result. + */ +export function parseOrientation(orientation) { + let rotate = 0; + let scaleX = 1; + let scaleY = 1; + + switch (orientation) { + // Flip horizontal + case 2: + scaleX = -1; + break; + + // Rotate left 180° + case 3: + rotate = -180; + break; + + // Flip vertical + case 4: + scaleY = -1; + break; + + // Flip vertical and rotate right 90° + case 5: + rotate = 90; + scaleY = -1; + break; + + // Rotate right 90° + case 6: + rotate = 90; + break; + + // Flip horizontal and rotate right 90° + case 7: + rotate = 90; + scaleX = -1; + break; + + // Rotate left 90° + case 8: + rotate = -90; + break; + + default: + } + + return { + rotate, + scaleX, + scaleY, + }; +} |