UNPKG

pkg-components

Version:
455 lines (369 loc) 11.5 kB
import DEFAULTS from './defaults' import TEMPLATE from './template' import render from './render' import preview from './preview' import events from './events' import handlers from './handlers' import change from './change' import methods from './methods' import { ACTION_ALL, CLASS_HIDDEN, CLASS_HIDE, CLASS_INVISIBLE, CLASS_MOVE, DATA_ACTION, EVENT_READY, MIME_TYPE_JPEG, NAMESPACE, REGEXP_DATA_URL, REGEXP_DATA_URL_JPEG, REGEXP_TAG_NAME, WINDOW } from './constants' import { addClass, addListener, addTimestamp, arrayBufferToDataURL, assign, dataURLToArrayBuffer, dispatchEvent, isCrossOriginURL, isFunction, isPlainObject, parseOrientation, removeClass, resetAndGetOrientation, setData } from './utilities' const AnotherCropper = WINDOW.Cropper class Cropper { /** * Create a new Cropper. * @param {Element} element - The target element for cropping. * @param {Object} [options={}] - The configuration options. */ constructor (element, options = {}) { if (!element || !REGEXP_TAG_NAME.test(element.tagName)) { throw new Error('The first argument is required and must be an <img> or <canvas> element.') } this.element = element this.options = assign({}, DEFAULTS, isPlainObject(options) && options) this.cropped = false this.disabled = false this.pointers = {} this.ready = false this.reloading = false this.replaced = false this.sized = false this.sizing = false this.init() } init () { const { element } = this const tagName = element.tagName.toLowerCase() let url if (element[NAMESPACE]) { return } element[NAMESPACE] = this if (tagName === 'img') { this.isImg = true // e.g.: "img/picture.jpg" url = element.getAttribute('src') || '' this.originalUrl = url // Stop when it's a blank image if (!url) { return } // e.g.: "https://example.com/img/picture.jpg" url = element.src } else if (tagName === 'canvas' && window.HTMLCanvasElement) { url = element.toDataURL() } this.load(url) } load (url) { if (!url) { return } this.url = url this.imageData = {} 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 } // 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() 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) } 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) } // The third parameter is required for avoiding side-effect (#682) xhr.open('GET', url, true) xhr.responseType = 'arraybuffer' xhr.withCredentials = element.crossOrigin === 'use-credentials' xhr.send() } read (arrayBuffer) { const { options, imageData } = this // 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) { // Generate a new URL which has the default orientation value this.url = arrayBufferToDataURL(arrayBuffer, MIME_TYPE_JPEG); ({ rotate, scaleX, scaleY } = parseOrientation(orientation)) } if (options.rotatable) { imageData.rotate = rotate } if (options.scalable) { imageData.scaleX = scaleX imageData.scaleY = scaleY } this.clone() } clone () { const { element, url } = this let { crossOrigin } = element let crossOriginUrl = url if (this.options.checkCrossOrigin && isCrossOriginURL(url)) { if (!crossOrigin) { crossOrigin = 'anonymous' } // Bust cache when there is not a "crossOrigin" property (#519) crossOriginUrl = addTimestamp(url) } this.crossOrigin = crossOrigin this.crossOriginUrl = crossOriginUrl const image = document.createElement('img') if (crossOrigin) { image.crossOrigin = crossOrigin } image.src = crossOriginUrl || url image.alt = element.alt || 'The image to crop' this.image = image image.onload = this.start.bind(this) image.onerror = this.stop.bind(this) addClass(image, CLASS_HIDE) element.parentNode.insertBefore(image, element.nextSibling) } start () { const { image } = this image.onload = null image.onerror = null this.sizing = true // 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.initialImageData = assign({}, this.imageData) 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 image.onload = null image.onerror = null image.parentNode.removeChild(image) this.image = null } build () { if (!this.sized || this.ready) { return } const { element, options, image } = this // Create cropper elements const container = element.parentNode const template = document.createElement('div') template.innerHTML = TEMPLATE const cropper = template.querySelector(`.${NAMESPACE}-container`) const canvas = cropper.querySelector(`.${NAMESPACE}-canvas`) const dragBox = cropper.querySelector(`.${NAMESPACE}-drag-box`) const cropBox = cropper.querySelector(`.${NAMESPACE}-crop-box`) const face = cropBox.querySelector(`.${NAMESPACE}-face`) this.container = container this.cropper = cropper this.canvas = canvas this.dragBox = dragBox this.cropBox = cropBox this.viewBox = cropper.querySelector(`.${NAMESPACE}-view-box`) this.face = face canvas.appendChild(image) // Hide the original image addClass(element, CLASS_HIDDEN) // Inserts the cropper after to the current image container.insertBefore(cropper, element.nextSibling) // Show the hidden image removeClass(image, CLASS_HIDE) 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 addClass(cropBox, CLASS_HIDDEN) if (!options.guides) { addClass(cropBox.getElementsByClassName(`${NAMESPACE}-dashed`), CLASS_HIDDEN) } if (!options.center) { addClass(cropBox.getElementsByClassName(`${NAMESPACE}-center`), CLASS_HIDDEN) } if (options.background) { addClass(cropper, `${NAMESPACE}-bg`) } if (!options.highlight) { addClass(face, CLASS_INVISIBLE) } if (options.cropBoxMovable) { addClass(face, CLASS_MOVE) setData(face, DATA_ACTION, ACTION_ALL) } if (!options.cropBoxResizable) { addClass(cropBox.getElementsByClassName(`${NAMESPACE}-line`), CLASS_HIDDEN) addClass(cropBox.getElementsByClassName(`${NAMESPACE}-point`), CLASS_HIDDEN) } this.render() this.ready = true this.setDragMode(options.dragMode) if (options.autoCrop) { this.crop() } this.setData(options.data) if (isFunction(options.ready)) { addListener(element, EVENT_READY, options.ready, { once: true }) } dispatchEvent(element, EVENT_READY) } unbuild () { if (!this.ready) { return } this.ready = false this.unbind() this.resetPreview() const { parentNode } = this.cropper if (parentNode) { parentNode.removeChild(this.cropper) } 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() } } /** * Get the no conflict cropper class. * @returns {Cropper} The cropper class. */ static noConflict () { window.Cropper = AnotherCropper return Cropper } /** * Change the default options. * @param {Object} options - The new default options. */ static setDefaults (options) { assign(DEFAULTS, isPlainObject(options) && options) } } assign(Cropper.prototype, render, preview, events, handlers, change, methods) export default Cropper