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
JavaScript
;
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