UNPKG

uppy

Version:

Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:

608 lines (530 loc) 17.1 kB
'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _Promise = typeof Promise === 'undefined' ? require('es6-promise').Promise : Promise; var throttle = require('lodash.throttle'); // we inline file-type module, as opposed to using the NPM version, // because of this https://github.com/sindresorhus/file-type/issues/78 // and https://github.com/sindresorhus/copy-text-to-clipboard/issues/5 var fileType = require('../vendor/file-type'); /** * A collection of small utility functions that help with dom manipulation, adding listeners, * promises and other good things. * * @module Utils */ function isTouchDevice() { return 'ontouchstart' in window || // works on most browsers navigator.maxTouchPoints; // works on IE10/11 and Surface } function truncateString(str, length) { if (str.length > length) { return str.substr(0, length / 2) + '...' + str.substr(str.length - length / 4, str.length); } return str; // more precise version if needed // http://stackoverflow.com/a/831583 } function secondsToTime(rawSeconds) { var hours = Math.floor(rawSeconds / 3600) % 24; var minutes = Math.floor(rawSeconds / 60) % 60; var seconds = Math.floor(rawSeconds % 60); return { hours: hours, minutes: minutes, seconds: seconds }; } /** * Converts list into array */ function toArray(list) { return Array.prototype.slice.call(list || [], 0); } /** * Returns a timestamp in the format of `hours:minutes:seconds` */ function getTimeStamp() { var date = new Date(); var hours = pad(date.getHours().toString()); var minutes = pad(date.getMinutes().toString()); var seconds = pad(date.getSeconds().toString()); return hours + ':' + minutes + ':' + seconds; } /** * Adds zero to strings shorter than two characters */ function pad(str) { return str.length !== 2 ? 0 + str : str; } /** * Takes a file object and turns it into fileID, by converting file.name to lowercase, * removing extra characters and adding type, size and lastModified * * @param {Object} file * @return {String} the fileID * */ function generateFileID(file) { // filter is needed to not join empty values with `-` return ['uppy', file.name ? file.name.toLowerCase().replace(/[^A-Z0-9]/ig, '') : '', file.type, file.data.size, file.data.lastModified].filter(function (val) { return val; }).join('-'); } /** * Runs an array of promise-returning functions in sequence. */ function runPromiseSequence(functions) { for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } var promise = _Promise.resolve(); functions.forEach(function (func) { promise = promise.then(function () { return func.apply(undefined, args); }); }); return promise; } function isPreviewSupported(fileType) { if (!fileType) return false; var fileTypeSpecific = fileType.split('/')[1]; // list of images that browsers can preview if (/^(jpeg|gif|png|svg|svg\+xml|bmp)$/.test(fileTypeSpecific)) { return true; } return false; } function getArrayBuffer(chunk) { return new _Promise(function (resolve, reject) { var reader = new FileReader(); reader.addEventListener('load', function (e) { // e.target.result is an ArrayBuffer resolve(e.target.result); }); reader.addEventListener('error', function (err) { console.error('FileReader error' + err); reject(err); }); // file-type only needs the first 4100 bytes reader.readAsArrayBuffer(chunk); }); } function getFileType(file) { var extensionsToMime = { 'md': 'text/markdown', 'markdown': 'text/markdown', 'mp4': 'video/mp4', 'mp3': 'audio/mp3', 'svg': 'image/svg+xml', 'jpg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif' }; var fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null; if (file.isRemote) { // some remote providers do not support file types var mime = file.type ? file.type : extensionsToMime[fileExtension]; return _Promise.resolve(mime); } // 1. try to determine file type from magic bytes with file-type module // this should be the most trustworthy way var chunk = file.data.slice(0, 4100); return getArrayBuffer(chunk).then(function (buffer) { var type = fileType(buffer); if (type && type.mime) { return type.mime; } // 2. if that’s no good, check if mime type is set in the file object if (file.type) { return file.type; } // 3. if that’s no good, see if we can map extension to a mime type if (fileExtension && extensionsToMime[fileExtension]) { return extensionsToMime[fileExtension]; } // if all fails, well, return empty return null; }).catch(function () { return null; }); } // TODO Check which types are actually supported in browsers. Chrome likes webm // from my testing, but we may need more. // We could use a library but they tend to contain dozens of KBs of mappings, // most of which will go unused, so not sure if that's worth it. var mimeToExtensions = { 'video/ogg': 'ogv', 'audio/ogg': 'ogg', 'video/webm': 'webm', 'audio/webm': 'webm', 'video/mp4': 'mp4', 'audio/mp3': 'mp3' }; function getFileTypeExtension(mimeType) { return mimeToExtensions[mimeType] || null; } /** * Takes a full filename string and returns an object {name, extension} * * @param {string} fullFileName * @return {object} {name, extension} */ function getFileNameAndExtension(fullFileName) { var re = /(?:\.([^.]+))?$/; var fileExt = re.exec(fullFileName)[1]; var fileName = fullFileName.replace('.' + fileExt, ''); return { name: fileName, extension: fileExt }; } /** * Check if a URL string is an object URL from `URL.createObjectURL`. * * @param {string} url * @return {boolean} */ function isObjectURL(url) { return url.indexOf('blob:') === 0; } function getProportionalHeight(img, width) { var aspect = img.width / img.height; return Math.round(width / aspect); } /** * Create a thumbnail for the given Uppy file object. * * @param {{data: Blob}} file * @param {number} width * @return {Promise} */ function createThumbnail(file, targetWidth) { var originalUrl = URL.createObjectURL(file.data); var onload = new _Promise(function (resolve, reject) { var image = new Image(); image.src = originalUrl; image.onload = function () { URL.revokeObjectURL(originalUrl); resolve(image); }; image.onerror = function () { // The onerror event is totally useless unfortunately, as far as I know URL.revokeObjectURL(originalUrl); reject(new Error('Could not create thumbnail')); }; }); return onload.then(function (image) { var targetHeight = getProportionalHeight(image, targetWidth); var canvas = resizeImage(image, targetWidth, targetHeight); return canvasToBlob(canvas, 'image/png'); }).then(function (blob) { return URL.createObjectURL(blob); }); } /** * Resize an image to the target `width` and `height`. * * Returns a Canvas with the resized image on it. */ function resizeImage(image, targetWidth, targetHeight) { var sourceWidth = image.width; var sourceHeight = image.height; if (targetHeight < image.height / 2) { var steps = Math.floor(Math.log(image.width / targetWidth) / Math.log(2)); var stepScaled = downScaleInSteps(image, steps); image = stepScaled.image; sourceWidth = stepScaled.sourceWidth; sourceHeight = stepScaled.sourceHeight; } var canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0, sourceWidth, sourceHeight, 0, 0, targetWidth, targetHeight); return canvas; } /** * Downscale an image by 50% `steps` times. */ function downScaleInSteps(image, steps) { var source = image; var currentWidth = source.width; var currentHeight = source.height; for (var i = 0; i < steps; i += 1) { var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); canvas.width = currentWidth / 2; canvas.height = currentHeight / 2; context.drawImage(source, // The entire source image. We pass width and height here, // because we reuse this canvas, and should only scale down // the part of the canvas that contains the previous scale step. 0, 0, currentWidth, currentHeight, // Draw to 50% size 0, 0, currentWidth / 2, currentHeight / 2); currentWidth /= 2; currentHeight /= 2; source = canvas; } return { image: source, sourceWidth: currentWidth, sourceHeight: currentHeight }; } /** * Save a <canvas> element's content to a Blob object. * * @param {HTMLCanvasElement} canvas * @return {Promise} */ function canvasToBlob(canvas, type, quality) { if (canvas.toBlob) { return new _Promise(function (resolve) { canvas.toBlob(resolve, type, quality); }); } return _Promise.resolve().then(function () { return dataURItoBlob(canvas.toDataURL(type, quality), {}); }); } function dataURItoBlob(dataURI, opts, toFile) { // get the base64 data var data = dataURI.split(',')[1]; // user may provide mime type, if not get it from data URI var mimeType = opts.mimeType || dataURI.split(',')[0].split(':')[1].split(';')[0]; // default to plain/text if data URI has no mimeType if (mimeType == null) { mimeType = 'plain/text'; } var binary = atob(data); var array = []; for (var i = 0; i < binary.length; i++) { array.push(binary.charCodeAt(i)); } // Convert to a File? if (toFile) { return new File([new Uint8Array(array)], opts.name || '', { type: mimeType }); } return new Blob([new Uint8Array(array)], { type: mimeType }); } function dataURItoFile(dataURI, opts) { return dataURItoBlob(dataURI, opts, true); } /** * Copies text to clipboard by creating an almost invisible textarea, * adding text there, then running execCommand('copy'). * Falls back to prompt() when the easy way fails (hello, Safari!) * From http://stackoverflow.com/a/30810322 * * @param {String} textToCopy * @param {String} fallbackString * @return {Promise} */ function copyToClipboard(textToCopy, fallbackString) { fallbackString = fallbackString || 'Copy the URL below'; return new _Promise(function (resolve) { var textArea = document.createElement('textarea'); textArea.setAttribute('style', { position: 'fixed', top: 0, left: 0, width: '2em', height: '2em', padding: 0, border: 'none', outline: 'none', boxShadow: 'none', background: 'transparent' }); textArea.value = textToCopy; document.body.appendChild(textArea); textArea.select(); var magicCopyFailed = function magicCopyFailed() { document.body.removeChild(textArea); window.prompt(fallbackString, textToCopy); resolve(); }; try { var successful = document.execCommand('copy'); if (!successful) { return magicCopyFailed('copy command unavailable'); } document.body.removeChild(textArea); return resolve(); } catch (err) { document.body.removeChild(textArea); return magicCopyFailed(err); } }); } function getSpeed(fileProgress) { if (!fileProgress.bytesUploaded) return 0; var timeElapsed = new Date() - fileProgress.uploadStarted; var uploadSpeed = fileProgress.bytesUploaded / (timeElapsed / 1000); return uploadSpeed; } function getBytesRemaining(fileProgress) { return fileProgress.bytesTotal - fileProgress.bytesUploaded; } function getETA(fileProgress) { if (!fileProgress.bytesUploaded) return 0; var uploadSpeed = getSpeed(fileProgress); var bytesRemaining = getBytesRemaining(fileProgress); var secondsRemaining = Math.round(bytesRemaining / uploadSpeed * 10) / 10; return secondsRemaining; } function prettyETA(seconds) { var time = secondsToTime(seconds); // Only display hours and minutes if they are greater than 0 but always // display minutes if hours is being displayed // Display a leading zero if the there is a preceding unit: 1m 05s, but 5s var hoursStr = time.hours ? time.hours + 'h ' : ''; var minutesVal = time.hours ? ('0' + time.minutes).substr(-2) : time.minutes; var minutesStr = minutesVal ? minutesVal + 'm ' : ''; var secondsVal = minutesVal ? ('0' + time.seconds).substr(-2) : time.seconds; var secondsStr = secondsVal + 's'; return '' + hoursStr + minutesStr + secondsStr; } /** * Check if an object is a DOM element. Duck-typing based on `nodeType`. * * @param {*} obj */ function isDOMElement(obj) { return obj && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object' && obj.nodeType === Node.ELEMENT_NODE; } /** * Find a DOM element. * * @param {Node|string} element * @return {Node|null} */ function findDOMElement(element) { if (typeof element === 'string') { return document.querySelector(element); } if ((typeof element === 'undefined' ? 'undefined' : _typeof(element)) === 'object' && isDOMElement(element)) { return element; } } /** * Find one or more DOM elements. * * @param {string} element * @return {Array|null} */ function findAllDOMElements(element) { if (typeof element === 'string') { var elements = [].slice.call(document.querySelectorAll(element)); return elements.length > 0 ? elements : null; } if ((typeof element === 'undefined' ? 'undefined' : _typeof(element)) === 'object' && isDOMElement(element)) { return [element]; } } function getSocketHost(url) { // get the host domain var regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/; var host = regex.exec(url)[1]; var socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws'; return socketProtocol + '://' + host; } function _emitSocketProgress(uploader, progressData, file) { var progress = progressData.progress, bytesUploaded = progressData.bytesUploaded, bytesTotal = progressData.bytesTotal; if (progress) { uploader.uppy.log('Upload progress: ' + progress); uploader.uppy.emit('upload-progress', file, { uploader: uploader, bytesUploaded: bytesUploaded, bytesTotal: bytesTotal }); } } var emitSocketProgress = throttle(_emitSocketProgress, 300, { leading: true, trailing: true }); function settle(promises) { var resolutions = []; var rejections = []; function resolved(value) { resolutions.push(value); } function rejected(error) { rejections.push(error); } var wait = _Promise.all(promises.map(function (promise) { return promise.then(resolved, rejected); })); return wait.then(function () { return { successful: resolutions, failed: rejections }; }); } /** * Limit the amount of simultaneously pending Promises. * Returns a function that, when passed a function `fn`, * will make sure that at most `limit` calls to `fn` are pending. * * @param {number} limit * @return {function()} */ function limitPromises(limit) { var pending = 0; var queue = []; return function (fn) { return function () { for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } var call = function call() { pending++; var promise = fn.apply(undefined, args); promise.then(onfinish, onfinish); return promise; }; if (pending >= limit) { return new _Promise(function (resolve, reject) { queue.push(function () { call().then(resolve, reject); }); }); } return call(); }; }; function onfinish() { pending--; var next = queue.shift(); if (next) next(); } } module.exports = { generateFileID: generateFileID, toArray: toArray, getTimeStamp: getTimeStamp, runPromiseSequence: runPromiseSequence, isTouchDevice: isTouchDevice, getFileNameAndExtension: getFileNameAndExtension, truncateString: truncateString, getFileTypeExtension: getFileTypeExtension, getFileType: getFileType, getArrayBuffer: getArrayBuffer, isPreviewSupported: isPreviewSupported, isObjectURL: isObjectURL, createThumbnail: createThumbnail, secondsToTime: secondsToTime, dataURItoBlob: dataURItoBlob, dataURItoFile: dataURItoFile, canvasToBlob: canvasToBlob, getSpeed: getSpeed, getBytesRemaining: getBytesRemaining, getETA: getETA, copyToClipboard: copyToClipboard, prettyETA: prettyETA, findDOMElement: findDOMElement, findAllDOMElements: findAllDOMElements, getSocketHost: getSocketHost, emitSocketProgress: emitSocketProgress, settle: settle, limitPromises: limitPromises }; //# sourceMappingURL=Utils.js.map