123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- const 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
- const 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) {
- const hours = Math.floor(rawSeconds / 3600) % 24
- const minutes = Math.floor(rawSeconds / 60) % 60
- const seconds = Math.floor(rawSeconds % 60)
- return { hours, minutes, 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(val => val).join('-')
- }
- /**
- * Runs an array of promise-returning functions in sequence.
- */
- function runPromiseSequence (functions, ...args) {
- let promise = Promise.resolve()
- functions.forEach((func) => {
- promise = promise.then(() => func(...args))
- })
- return promise
- }
- function isPreviewSupported (fileTypeSpecific) {
- // 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) {
- const emptyFileType = ['', '']
- const extensionsToMime = {
- 'md': 'text/markdown',
- 'markdown': 'text/markdown',
- 'mp4': 'video/mp4',
- 'mp3': 'audio/mp3'
- }
- const fileExtension = getFileNameAndExtension(file.name)[1]
- if (file.isRemote) {
- // some providers do not support for file types
- let mime = file.type ? file.type : extensionsToMime[fileExtension]
- const type = mime ? mime.split('/') : emptyFileType
- return Promise.resolve(type)
- }
- // 1. try to determine file type from magic bytes with file-type module
- // this should be the most trustworthy way
- const chunk = file.data.slice(0, 4100)
- return getArrayBuffer(chunk)
- .then((buffer) => {
- const type = fileType(buffer)
- if (type && type.mime) {
- return type.mime.split('/')
- }
- // 2. if that’s no good, check if mime type is set in the file object
- if (file.type) {
- return file.type.split('/')
- }
- // 3. if that’s no good, see if we can map extension to a mime type
- if (extensionsToMime[fileExtension]) {
- return extensionsToMime[fileExtension].split('/')
- }
- // if all fails, well, return empty
- return emptyFileType
- })
- .catch(() => {
- return emptyFileType
- })
- // if (file.type) {
- // return Promise.resolve(file.type.split('/'))
- // }
- // return mime.lookup(file.name)
- // return file.type ? file.type.split('/') : ['', '']
- }
- // 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.
- const 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
- }
- // returns [fileName, fileExt]
- function getFileNameAndExtension (fullFileName) {
- var re = /(?:\.([^.]+))?$/
- var fileExt = re.exec(fullFileName)[1]
- var fileName = fullFileName.replace('.' + fileExt, '')
- return [fileName, fileExt]
- }
- function supportsMediaRecorder () {
- return typeof MediaRecorder === 'function' && !!MediaRecorder.prototype &&
- typeof MediaRecorder.prototype.start === 'function'
- }
- /**
- * 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) {
- const 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) {
- const originalUrl = URL.createObjectURL(file.data)
- const onload = new Promise((resolve, reject) => {
- const image = new Image()
- image.src = originalUrl
- image.onload = () => {
- URL.revokeObjectURL(originalUrl)
- resolve(image)
- }
- image.onerror = () => {
- // 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((image) => {
- const targetHeight = getProportionalHeight(image, targetWidth)
- const canvas = resizeImage(image, targetWidth, targetHeight)
- return canvasToBlob(canvas, 'image/png')
- }).then((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) {
- let sourceWidth = image.width
- let sourceHeight = image.height
- if (targetHeight < image.height / 2) {
- const steps = Math.floor(Math.log(image.width / targetWidth) / Math.log(2))
- const stepScaled = downScaleInSteps(image, steps)
- image = stepScaled.image
- sourceWidth = stepScaled.sourceWidth
- sourceHeight = stepScaled.sourceHeight
- }
- const canvas = document.createElement('canvas')
- canvas.width = targetWidth
- canvas.height = targetHeight
- const 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) {
- let source = image
- let currentWidth = source.width
- let currentHeight = source.height
- for (let i = 0; i < steps; i += 1) {
- const canvas = document.createElement('canvas')
- const 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((resolve) => {
- canvas.toBlob(resolve, type, quality)
- })
- }
- return Promise.resolve().then(() => {
- 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((resolve) => {
- const 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()
- const magicCopyFailed = () => {
- document.body.removeChild(textArea)
- window.prompt(fallbackString, textToCopy)
- resolve()
- }
- try {
- const 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
- const timeElapsed = (new Date()) - fileProgress.uploadStarted
- const uploadSpeed = fileProgress.bytesUploaded / (timeElapsed / 1000)
- return uploadSpeed
- }
- function getBytesRemaining (fileProgress) {
- return fileProgress.bytesTotal - fileProgress.bytesUploaded
- }
- function getETA (fileProgress) {
- if (!fileProgress.bytesUploaded) return 0
- const uploadSpeed = getSpeed(fileProgress)
- const bytesRemaining = getBytesRemaining(fileProgress)
- const secondsRemaining = Math.round(bytesRemaining / uploadSpeed * 10) / 10
- return secondsRemaining
- }
- function prettyETA (seconds) {
- const 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
- const hoursStr = time.hours ? time.hours + 'h ' : ''
- const minutesVal = time.hours ? ('0' + time.minutes).substr(-2) : time.minutes
- const minutesStr = minutesVal ? minutesVal + 'm ' : ''
- const secondsVal = minutesVal ? ('0' + time.seconds).substr(-2) : time.seconds
- const 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 === '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 === 'object' && isDOMElement(element)) {
- return element
- }
- }
- /**
- * Find one or more DOM elements.
- *
- * @param {string} element
- * @return {Array|null}
- */
- function findAllDOMElements (element) {
- if (typeof element === 'string') {
- const elements = [].slice.call(document.querySelectorAll(element))
- return elements.length > 0 ? elements : null
- }
- if (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) {
- const {progress, bytesUploaded, bytesTotal} = progressData
- if (progress) {
- uploader.core.log(`Upload progress: ${progress}`)
- uploader.core.emitter.emit('core:upload-progress', {
- uploader,
- id: file.id,
- bytesUploaded: bytesUploaded,
- bytesTotal: bytesTotal
- })
- }
- }
- const emitSocketProgress = throttle(_emitSocketProgress, 300, {leading: true, trailing: true})
- function settle (promises) {
- const resolutions = []
- const rejections = []
- function resolved (value) {
- resolutions.push(value)
- }
- function rejected (error) {
- rejections.push(error)
- }
- const wait = Promise.all(
- promises.map((promise) => promise.then(resolved, rejected))
- )
- return wait.then(() => {
- if (rejections.length === promises.length) {
- // Very ad-hoc multiple-error reporting, should wrap this in a
- // CombinedError or whatever kind of error class instead.
- const error = rejections[0]
- error.errors = rejections
- return Promise.reject(error)
- }
- return {
- successful: resolutions,
- failed: rejections
- }
- })
- }
- module.exports = {
- generateFileID,
- toArray,
- getTimeStamp,
- runPromiseSequence,
- supportsMediaRecorder,
- isTouchDevice,
- getFileNameAndExtension,
- truncateString,
- getFileTypeExtension,
- getFileType,
- getArrayBuffer,
- isPreviewSupported,
- isObjectURL,
- createThumbnail,
- secondsToTime,
- dataURItoBlob,
- dataURItoFile,
- getSpeed,
- getBytesRemaining,
- getETA,
- copyToClipboard,
- prettyETA,
- findDOMElement,
- findAllDOMElements,
- getSocketHost,
- emitSocketProgress,
- settle
- }
|