Forráskód Böngészése

Merge pull request #899 from transloadit/feature/split-utils

Split utils into separate files.
Artur Paikin 6 éve
szülő
commit
ea221c4c34
73 módosított fájl, 925 hozzáadás és 526 törlés
  1. 1 1
      package.json
  2. 11 7
      src/core/Core.js
  3. 3 4
      src/core/Core.test.js
  4. 1 1
      src/core/Plugin.js
  5. 0 468
      src/core/Utils.js
  6. 0 3
      src/core/Utils.test.js
  7. 3 5
      src/plugins/AwsS3/Multipart.js
  8. 1 1
      src/plugins/AwsS3/index.js
  9. 1 1
      src/plugins/Dashboard/Dashboard.js
  10. 3 3
      src/plugins/Dashboard/FileItem.js
  11. 2 1
      src/plugins/Dashboard/index.js
  12. 1 1
      src/plugins/DragDrop/index.js
  13. 1 1
      src/plugins/FileInput.js
  14. 1 1
      src/plugins/Form.js
  15. 3 3
      src/plugins/StatusBar/index.js
  16. 5 3
      src/plugins/ThumbnailGenerator/index.js
  17. 4 6
      src/plugins/Tus.js
  18. 1 1
      src/plugins/Url/index.js
  19. 2 4
      src/plugins/Webcam/index.js
  20. 4 6
      src/plugins/XHRUpload.js
  21. 18 0
      src/utils/canvasToBlob.js
  22. 51 0
      src/utils/copyToClipboard.js
  23. 7 0
      src/utils/copyToClipboard.test.js
  24. 25 0
      src/utils/dataURItoBlob.js
  25. 11 0
      src/utils/dataURItoBlob.test.js
  26. 5 0
      src/utils/dataURItoFile.js
  27. 12 0
      src/utils/dataURItoFile.test.js
  28. 15 0
      src/utils/emitSocketProgress.js
  29. 18 0
      src/utils/findAllDOMElements.js
  30. 17 0
      src/utils/findDOMElement.js
  31. 18 0
      src/utils/generateFileID.js
  32. 18 0
      src/utils/generateFileID.test.js
  33. 15 0
      src/utils/getArrayBuffer.js
  34. 30 0
      src/utils/getArrayBuffer.test.js
  35. 3 0
      src/utils/getBytesRemaining.js
  36. 11 0
      src/utils/getBytesRemaining.test.js
  37. 12 0
      src/utils/getETA.js
  38. 14 0
      src/utils/getETA.test.js
  39. 15 0
      src/utils/getFileNameAndExtension.js
  40. 17 0
      src/utils/getFileNameAndExtension.test.js
  41. 24 0
      src/utils/getFileType.js
  42. 47 0
      src/utils/getFileType.test.js
  43. 16 0
      src/utils/getFileTypeExtension.js
  44. 13 0
      src/utils/getFileTypeExtension.test.js
  45. 8 0
      src/utils/getSocketHost.js
  46. 9 0
      src/utils/getSocketHost.test.js
  47. 7 0
      src/utils/getSpeed.js
  48. 13 0
      src/utils/getSpeed.test.js
  49. 17 0
      src/utils/getTimeStamp.js
  50. 8 0
      src/utils/isDOMElement.js
  51. 9 0
      src/utils/isObjectURL.js
  52. 10 0
      src/utils/isObjectURL.test.js
  53. 9 0
      src/utils/isPreviewSupported.js
  54. 11 0
      src/utils/isPreviewSupported.test.js
  55. 4 0
      src/utils/isTouchDevice.js
  56. 23 0
      src/utils/isTouchDevice.test.js
  57. 36 0
      src/utils/limitPromises.js
  58. 47 0
      src/utils/limitPromises.test.js
  59. 36 0
      src/utils/mimeTypes.js
  60. 16 0
      src/utils/prettyETA.js
  61. 12 0
      src/utils/prettyETA.test.js
  62. 10 0
      src/utils/runPromiseSequence.js
  63. 15 0
      src/utils/runPromiseSequence.test.js
  64. 0 0
      src/utils/sampleImageDataURI.js
  65. 7 0
      src/utils/secondsToTime.js
  66. 29 0
      src/utils/secondsToTime.test.js
  67. 21 0
      src/utils/settle.js
  68. 28 0
      src/utils/settle.test.js
  69. 6 0
      src/utils/toArray.js
  70. 22 0
      src/utils/toArray.test.js
  71. 9 0
      src/utils/truncateString.js
  72. 16 0
      src/utils/truncateString.test.js
  73. 7 5
      src/views/ProviderView/index.js

+ 1 - 1
package.json

@@ -133,7 +133,7 @@
     "build:gzip": "node ./bin/gzip.js",
     "size": "echo 'JS Bundle mingz:' && cat ./dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./dist/uppy.min.css | gzip | wc -c",
     "build:js": "npm-run-all build:bundle build:lib",
-    "build:lib": "babel --version && babel src --source-maps -d lib",
+    "build:lib": "babel --version && babel src --source-maps -d lib --ignore '*.test.js'",
     "build": "npm-run-all --parallel build:js build:css --serial build:gzip size",
     "clean": "rm -rf lib && rm -rf dist",
     "lint:fix": "eslint src test website/scripts website/build-examples.js website/update.js website/themes/uppy/source/js/common.js --fix",

+ 11 - 7
src/core/Core.js

@@ -1,4 +1,3 @@
-const Utils = require('../core/Utils')
 const Translator = require('../core/Translator')
 const ee = require('namespace-emitter')
 const cuid = require('cuid')
@@ -6,6 +5,11 @@ const cuid = require('cuid')
 const prettyBytes = require('prettier-bytes')
 const match = require('mime-match')
 const DefaultStore = require('../store/DefaultStore')
+const getFileType = require('../utils/getFileType')
+const getFileNameAndExtension = require('../utils/getFileNameAndExtension')
+const generateFileID = require('../utils/generateFileID')
+const isObjectURL = require('../utils/isObjectURL')
+const getTimeStamp = require('../utils/getTimeStamp')
 
 /**
  * Uppy Core module.
@@ -397,7 +401,7 @@ class Uppy {
       file = onBeforeFileAddedResult
     }
 
-    const fileType = Utils.getFileType(file)
+    const fileType = getFileType(file)
     let fileName
     if (file.name) {
       fileName = file.name
@@ -406,10 +410,10 @@ class Uppy {
     } else {
       fileName = 'noname'
     }
-    const fileExtension = Utils.getFileNameAndExtension(fileName).extension
+    const fileExtension = getFileNameAndExtension(fileName).extension
     const isRemote = file.isRemote || false
 
-    const fileID = Utils.generateFileID(file)
+    const fileID = generateFileID(file)
 
     const meta = file.meta || {}
     meta.name = fileName
@@ -497,7 +501,7 @@ class Uppy {
     this.log(`File removed: ${removedFile.id}`)
 
     // Clean up object URLs.
-    if (removedFile.preview && Utils.isObjectURL(removedFile.preview)) {
+    if (removedFile.preview && isObjectURL(removedFile.preview)) {
       URL.revokeObjectURL(removedFile.preview)
     }
 
@@ -974,7 +978,7 @@ class Uppy {
       return
     }
 
-    let message = `[Uppy] [${Utils.getTimeStamp()}] ${msg}`
+    let message = `[Uppy] [${getTimeStamp()}] ${msg}`
 
     window['uppyLog'] = window['uppyLog'] + '\n' + 'DEBUG LOG: ' + msg
 
@@ -991,7 +995,7 @@ class Uppy {
     if (msg === `${msg}`) {
       console.log(message)
     } else {
-      message = `[Uppy] [${Utils.getTimeStamp()}]`
+      message = `[Uppy] [${getTimeStamp()}]`
       console.log(message)
       console.dir(msg)
     }

+ 3 - 4
src/core/Core.test.js

@@ -1,7 +1,6 @@
 const fs = require('fs')
 const path = require('path')
 const Core = require('./Core')
-const utils = require('./Utils')
 const Plugin = require('./Plugin')
 const AcquirerPlugin1 = require('../../test/mocks/acquirerPlugin1')
 const AcquirerPlugin2 = require('../../test/mocks/acquirerPlugin2')
@@ -12,15 +11,15 @@ const InvalidPluginWithoutType = require('../../test/mocks/invalidPluginWithoutT
 jest.mock('cuid', () => {
   return () => 'cjd09qwxb000dlql4tp4doz8h'
 })
+jest.mock('../utils/findDOMElement', () => {
+  return () => null
+})
 
 const sampleImage = fs.readFileSync(path.join(__dirname, '../../test/resources/image.jpg'))
 
 describe('src/Core', () => {
   const RealCreateObjectUrl = global.URL.createObjectURL
   beforeEach(() => {
-    jest.spyOn(utils, 'findDOMElement').mockImplementation(path => {
-      return 'some config...'
-    })
     global.URL.createObjectURL = jest.fn().mockReturnValue('newUrl')
   })
 

+ 1 - 1
src/core/Plugin.js

@@ -1,5 +1,5 @@
 const preact = require('preact')
-const { findDOMElement } = require('../core/Utils')
+const findDOMElement = require('../utils/findDOMElement')
 
 /**
  * Defer a frequent call to the microtask queue.

+ 0 - 468
src/core/Utils.js

@@ -1,468 +0,0 @@
-const throttle = require('lodash.throttle')
-const mimeTypes = require('./mime-types.js')
-
-/**
- * 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 (fileType) {
-  if (!fileType) return false
-  const 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) {
-  const fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null
-
-  if (file.isRemote) {
-    // some remote providers do not support file types
-    return file.type ? file.type : mimeTypes[fileExtension]
-  }
-
-  // check if mime type is set in the file object
-  if (file.type) {
-    return file.type
-  }
-
-  // see if we can map extension to a mime type
-  if (fileExtension && mimeTypes[fileExtension]) {
-    return mimeTypes[fileExtension]
-  }
-
-  // if all fails, well, return empty
-  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.
-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
-}
-
-/**
-* 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
-}
-
-/**
- * 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.uppy.log(`Upload progress: ${progress}`)
-    uploader.uppy.emit('upload-progress', file, {
-      uploader,
-      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(() => {
-    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) {
-  let pending = 0
-  const queue = []
-  return (fn) => {
-    return (...args) => {
-      const call = () => {
-        pending++
-        const promise = fn(...args)
-        promise.then(onfinish, onfinish)
-        return promise
-      }
-
-      if (pending >= limit) {
-        return new Promise((resolve, reject) => {
-          queue.push(() => {
-            call().then(resolve, reject)
-          })
-        })
-      }
-      return call()
-    }
-  }
-  function onfinish () {
-    pending--
-    const next = queue.shift()
-    if (next) next()
-  }
-}
-
-module.exports = {
-  generateFileID,
-  toArray,
-  getTimeStamp,
-  runPromiseSequence,
-  isTouchDevice,
-  getFileNameAndExtension,
-  truncateString,
-  getFileTypeExtension,
-  getFileType,
-  getArrayBuffer,
-  isPreviewSupported,
-  isObjectURL,
-  secondsToTime,
-  dataURItoBlob,
-  dataURItoFile,
-  canvasToBlob,
-  getSpeed,
-  getBytesRemaining,
-  getETA,
-  copyToClipboard,
-  prettyETA,
-  findDOMElement,
-  findAllDOMElements,
-  getSocketHost,
-  emitSocketProgress,
-  settle,
-  limitPromises
-}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 3
src/core/Utils.test.js


+ 3 - 5
src/plugins/AwsS3/Multipart.js

@@ -1,11 +1,9 @@
 const Plugin = require('../../core/Plugin')
 const RequestClient = require('../../server/RequestClient')
 const UppySocket = require('../../core/UppySocket')
-const {
-  emitSocketProgress,
-  getSocketHost,
-  limitPromises
-} = require('../../core/Utils')
+const emitSocketProgress = require('../../utils/emitSocketProgress')
+const getSocketHost = require('../../utils/getSocketHost')
+const limitPromises = require('../../utils/limitPromises')
 const Uploader = require('./MultipartUploader')
 
 /**

+ 1 - 1
src/plugins/AwsS3/index.js

@@ -1,7 +1,7 @@
 const resolveUrl = require('resolve-url')
 const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
-const { limitPromises } = require('../../core/Utils')
+const limitPromises = require('../../utils/limitPromises')
 const XHRUpload = require('../XHRUpload')
 
 function isXml (xhr) {

+ 1 - 1
src/plugins/Dashboard/Dashboard.js

@@ -2,7 +2,7 @@ const FileList = require('./FileList')
 const Tabs = require('./Tabs')
 const FileCard = require('./FileCard')
 const classNames = require('classnames')
-const { isTouchDevice } = require('../../core/Utils')
+const isTouchDevice = require('../../utils/isTouchDevice')
 const { h } = require('preact')
 
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog

+ 3 - 3
src/plugins/Dashboard/FileItem.js

@@ -1,6 +1,6 @@
-const { getFileNameAndExtension,
-         truncateString,
-         copyToClipboard } = require('../../core/Utils')
+const getFileNameAndExtension = require('../../utils/getFileNameAndExtension')
+const truncateString = require('../../utils/truncateString')
+const copyToClipboard = require('../../utils/copyToClipboard')
 const prettyBytes = require('prettier-bytes')
 const FileItemProgress = require('./FileItemProgress')
 const getFileTypeIcon = require('./getFileTypeIcon')

+ 2 - 1
src/plugins/Dashboard/index.js

@@ -5,7 +5,8 @@ const DashboardUI = require('./Dashboard')
 const StatusBar = require('../StatusBar')
 const Informer = require('../Informer')
 const ThumbnailGenerator = require('../ThumbnailGenerator')
-const { findAllDOMElements, toArray } = require('../../core/Utils')
+const findAllDOMElements = require('../../utils/findAllDOMElements')
+const toArray = require('../../utils/toArray')
 const prettyBytes = require('prettier-bytes')
 const { defaultTabIcon } = require('./icons')
 

+ 1 - 1
src/plugins/DragDrop/index.js

@@ -1,6 +1,6 @@
 const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
-const { toArray } = require('../../core/Utils')
+const toArray = require('../../utils/toArray')
 const dragDrop = require('drag-drop')
 const { h } = require('preact')
 

+ 1 - 1
src/plugins/FileInput.js

@@ -1,5 +1,5 @@
 const Plugin = require('../core/Plugin')
-const { toArray } = require('../core/Utils')
+const toArray = require('../utils/toArray')
 const Translator = require('../core/Translator')
 const { h } = require('preact')
 

+ 1 - 1
src/plugins/Form.js

@@ -1,5 +1,5 @@
 const Plugin = require('../core/Plugin')
-const { findDOMElement } = require('../core/Utils')
+const findDOMElement = require('../utils/findDOMElement')
 // Rollup uses get-form-data's ES modules build, and rollup-plugin-commonjs automatically resolves `.default`.
 // So, if we are being built using rollup, this require() won't have a `.default` property.
 const getFormData = require('get-form-data').default || require('get-form-data')

+ 3 - 3
src/plugins/StatusBar/index.js

@@ -2,9 +2,9 @@ const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
 const StatusBarUI = require('./StatusBar')
 const statusBarStates = require('./StatusBarStates')
-const { getSpeed } = require('../../core/Utils')
-const { getBytesRemaining } = require('../../core/Utils')
-const { prettyETA } = require('../../core/Utils')
+const getSpeed = require('../../utils/getSpeed')
+const getBytesRemaining = require('../../utils/getBytesRemaining')
+const prettyETA = require('../../utils/prettyETA')
 const prettyBytes = require('prettier-bytes')
 
 /**

+ 5 - 3
src/plugins/ThumbnailGenerator/index.js

@@ -1,5 +1,7 @@
 const Plugin = require('../../core/Plugin')
-const Utils = require('../../core/Utils')
+const dataURItoBlob = require('../../utils/dataURItoBlob')
+const isPreviewSupported = require('../../utils/isPreviewSupported')
+
 /**
  * The Thumbnail Generator plugin
  *
@@ -142,7 +144,7 @@ module.exports = class ThumbnailGenerator extends Plugin {
       })
     }
     return Promise.resolve().then(() => {
-      return Utils.dataURItoBlob(canvas.toDataURL(type, quality), {})
+      return dataURItoBlob(canvas.toDataURL(type, quality), {})
     })
   }
 
@@ -180,7 +182,7 @@ module.exports = class ThumbnailGenerator extends Plugin {
   }
 
   requestThumbnail (file) {
-    if (Utils.isPreviewSupported(file.type) && !file.isRemote) {
+    if (isPreviewSupported(file.type) && !file.isRemote) {
       return this.createThumbnail(file, this.opts.thumbnailWidth)
         .then(preview => {
           this.setPreviewURL(file.id, preview)

+ 4 - 6
src/plugins/Tus.js

@@ -2,12 +2,10 @@ const Plugin = require('../core/Plugin')
 const tus = require('tus-js-client')
 const UppySocket = require('../core/UppySocket')
 const { Provider, RequestClient } = require('../server')
-const {
-  emitSocketProgress,
-  getSocketHost,
-  settle,
-  limitPromises
-} = require('../core/Utils')
+const emitSocketProgress = require('../utils/emitSocketProgress')
+const getSocketHost = require('../utils/getSocketHost')
+const settle = require('../utils/settle')
+const limitPromises = require('../utils/limitPromises')
 require('whatwg-fetch')
 
 // Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13

+ 1 - 1
src/plugins/Url/index.js

@@ -3,7 +3,7 @@ const Translator = require('../../core/Translator')
 const { h } = require('preact')
 const { RequestClient } = require('../../server')
 const UrlUI = require('./UrlUI.js')
-const { toArray } = require('../../core/Utils')
+const toArray = require('../../utils/toArray')
 
 /**
  * Url

+ 2 - 4
src/plugins/Webcam/index.js

@@ -1,10 +1,8 @@
 const { h } = require('preact')
 const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
-const {
-  getFileTypeExtension,
-  canvasToBlob
-} = require('../../core/Utils')
+const getFileTypeExtension = require('../../utils/getFileTypeExtension')
+const canvasToBlob = require('../../utils/canvasToBlob')
 const supportsMediaRecorder = require('./supportsMediaRecorder')
 const CameraIcon = require('./CameraIcon')
 const CameraScreen = require('./CameraScreen')

+ 4 - 6
src/plugins/XHRUpload.js

@@ -3,12 +3,10 @@ const cuid = require('cuid')
 const Translator = require('../core/Translator')
 const UppySocket = require('../core/UppySocket')
 const Provider = require('../server/Provider')
-const {
-  emitSocketProgress,
-  getSocketHost,
-  settle,
-  limitPromises
-} = require('../core/Utils')
+const emitSocketProgress = require('../utils/emitSocketProgress')
+const getSocketHost = require('../utils/getSocketHost')
+const settle = require('../utils/settle')
+const limitPromises = require('../utils/limitPromises')
 
 function buildResponseError (xhr, error) {
   // No error message

+ 18 - 0
src/utils/canvasToBlob.js

@@ -0,0 +1,18 @@
+const dataURItoBlob = require('./dataURItoBlob')
+
+/**
+ * Save a <canvas> element's content to a Blob object.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @return {Promise}
+ */
+module.exports = 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), {})
+  })
+}

+ 51 - 0
src/utils/copyToClipboard.js

@@ -0,0 +1,51 @@
+/**
+ * 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}
+ */
+module.exports = 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)
+    }
+  })
+}

+ 7 - 0
src/utils/copyToClipboard.test.js

@@ -0,0 +1,7 @@
+const copyToClipboard = require('./copyToClipboard')
+
+describe('copyToClipboard', () => {
+  xit('should copy the specified text to the clipboard', () => {
+    expect(typeof copyToClipboard).toBe('function')
+  })
+})

+ 25 - 0
src/utils/dataURItoBlob.js

@@ -0,0 +1,25 @@
+module.exports = 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})
+}

+ 11 - 0
src/utils/dataURItoBlob.test.js

@@ -0,0 +1,11 @@
+const dataURItoBlob = require('./dataURItoBlob')
+const sampleImageDataURI = require('./sampleImageDataURI')
+
+describe('dataURItoBlob', () => {
+  it('should convert a data uri to a blob', () => {
+    const blob = dataURItoBlob(sampleImageDataURI, {})
+    expect(blob instanceof Blob).toEqual(true)
+    expect(blob.size).toEqual(9348)
+    expect(blob.type).toEqual('image/jpeg')
+  })
+})

+ 5 - 0
src/utils/dataURItoFile.js

@@ -0,0 +1,5 @@
+const dataURItoBlob = require('./dataURItoBlob')
+
+module.exports = function dataURItoFile (dataURI, opts) {
+  return dataURItoBlob(dataURI, opts, true)
+}

+ 12 - 0
src/utils/dataURItoFile.test.js

@@ -0,0 +1,12 @@
+const dataURItoFile = require('./dataURItoFile')
+const sampleImageDataURI = require('./sampleImageDataURI')
+
+describe('dataURItoFile', () => {
+  it('should convert a data uri to a file', () => {
+    const file = dataURItoFile(sampleImageDataURI, { name: 'foo.jpg' })
+    expect(file instanceof File).toEqual(true)
+    expect(file.size).toEqual(9348)
+    expect(file.type).toEqual('image/jpeg')
+    expect(file.name).toEqual('foo.jpg')
+  })
+})

+ 15 - 0
src/utils/emitSocketProgress.js

@@ -0,0 +1,15 @@
+const throttle = require('lodash.throttle')
+
+function _emitSocketProgress (uploader, progressData, file) {
+  const { progress, bytesUploaded, bytesTotal } = progressData
+  if (progress) {
+    uploader.uppy.log(`Upload progress: ${progress}`)
+    uploader.uppy.emit('upload-progress', file, {
+      uploader,
+      bytesUploaded: bytesUploaded,
+      bytesTotal: bytesTotal
+    })
+  }
+}
+
+module.exports = throttle(_emitSocketProgress, 300, {leading: true, trailing: true})

+ 18 - 0
src/utils/findAllDOMElements.js

@@ -0,0 +1,18 @@
+const isDOMElement = require('./isDOMElement')
+
+/**
+ * Find one or more DOM elements.
+ *
+ * @param {string} element
+ * @return {Array|null}
+ */
+module.exports = 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]
+  }
+}

+ 17 - 0
src/utils/findDOMElement.js

@@ -0,0 +1,17 @@
+const isDOMElement = require('./isDOMElement')
+
+/**
+ * Find a DOM element.
+ *
+ * @param {Node|string} element
+ * @return {Node|null}
+ */
+module.exports = function findDOMElement (element) {
+  if (typeof element === 'string') {
+    return document.querySelector(element)
+  }
+
+  if (typeof element === 'object' && isDOMElement(element)) {
+    return element
+  }
+}

+ 18 - 0
src/utils/generateFileID.js

@@ -0,0 +1,18 @@
+/**
+ * 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
+ *
+ */
+module.exports = 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('-')
+}

+ 18 - 0
src/utils/generateFileID.test.js

@@ -0,0 +1,18 @@
+const generateFileID = require('./generateFileID')
+
+describe('generateFileID', () => {
+  it('should take the filename object and produce a lowercase file id made up of uppy- prefix, file name (cleaned up to be lowercase, letters and numbers only), type, size and lastModified date', () => {
+    const fileObj = {
+      name: 'fOo0Fi@£$.jpg',
+      type: 'image/jpeg',
+      data: {
+        lastModified: 1498510508000,
+        size: 2271173
+      }
+    }
+
+    expect(generateFileID(fileObj)).toEqual(
+      'uppy-foo0fijpg-image/jpeg-2271173-1498510508000'
+    )
+  })
+})

+ 15 - 0
src/utils/getArrayBuffer.js

@@ -0,0 +1,15 @@
+module.exports = 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)
+  })
+}

+ 30 - 0
src/utils/getArrayBuffer.test.js

@@ -0,0 +1,30 @@
+const getArrayBuffer = require('./getArrayBuffer')
+
+describe('getArrayBuffer', () => {
+  beforeEach(() => {
+    global.FileReader = class FileReader {
+      addEventListener (e, cb) {
+        if (e === 'load') {
+          this.loadCb = cb
+        }
+        if (e === 'error') {
+          this.errorCb = cb
+        }
+      }
+      readAsArrayBuffer (chunk) {
+        this.loadCb({ target: { result: new ArrayBuffer(8) } })
+      }
+      }
+  })
+
+  afterEach(() => {
+    global.FileReader = undefined
+  })
+
+  it('should return a promise that resolves with the specified chunk', () => {
+    return getArrayBuffer('abcde').then(buffer => {
+      expect(typeof buffer).toEqual('object')
+      expect(buffer.byteLength).toEqual(8)
+    })
+  })
+})

+ 3 - 0
src/utils/getBytesRemaining.js

@@ -0,0 +1,3 @@
+module.exports = function getBytesRemaining (fileProgress) {
+  return fileProgress.bytesTotal - fileProgress.bytesUploaded
+}

+ 11 - 0
src/utils/getBytesRemaining.test.js

@@ -0,0 +1,11 @@
+const getBytesRemaining = require('./getBytesRemaining')
+
+describe('getBytesRemaining', () => {
+  it('should calculate the bytes remaining given a fileProgress object', () => {
+    const fileProgress = {
+      bytesUploaded: 1024,
+      bytesTotal: 3096
+    }
+    expect(getBytesRemaining(fileProgress)).toEqual(2072)
+  })
+})

+ 12 - 0
src/utils/getETA.js

@@ -0,0 +1,12 @@
+const getSpeed = require('./getSpeed')
+const getBytesRemaining = require('./getBytesRemaining')
+
+module.exports = 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
+}

+ 14 - 0
src/utils/getETA.test.js

@@ -0,0 +1,14 @@
+const getETA = require('./getETA')
+
+describe('getETA', () => {
+  it('should get the ETA remaining based on a fileProgress object', () => {
+    const dateNow = new Date()
+    const date5SecondsAgo = new Date(dateNow.getTime() - 5 * 1000)
+    const fileProgress = {
+      bytesUploaded: 1024,
+      bytesTotal: 3096,
+      uploadStarted: date5SecondsAgo
+    }
+    expect(getETA(fileProgress)).toEqual(10.1)
+  })
+})

+ 15 - 0
src/utils/getFileNameAndExtension.js

@@ -0,0 +1,15 @@
+/**
+* Takes a full filename string and returns an object {name, extension}
+*
+* @param {string} fullFileName
+* @return {object} {name, extension}
+*/
+module.exports = function getFileNameAndExtension (fullFileName) {
+  var re = /(?:\.([^.]+))?$/
+  var fileExt = re.exec(fullFileName)[1]
+  var fileName = fullFileName.replace('.' + fileExt, '')
+  return {
+    name: fileName,
+    extension: fileExt
+  }
+}

+ 17 - 0
src/utils/getFileNameAndExtension.test.js

@@ -0,0 +1,17 @@
+const getFileNameAndExtension = require('./getFileNameAndExtension')
+
+describe('getFileNameAndExtension', () => {
+  it('should return the filename and extension as an array', () => {
+    expect(getFileNameAndExtension('fsdfjodsuf23rfw.jpg')).toEqual({
+      name: 'fsdfjodsuf23rfw',
+      extension: 'jpg'
+    })
+  })
+
+  it('should handle invalid filenames', () => {
+    expect(getFileNameAndExtension('fsdfjodsuf23rfw')).toEqual({
+      name: 'fsdfjodsuf23rfw',
+      extension: undefined
+    })
+  })
+})

+ 24 - 0
src/utils/getFileType.js

@@ -0,0 +1,24 @@
+const getFileNameAndExtension = require('./getFileNameAndExtension')
+const mimeTypes = require('./mimeTypes')
+
+module.exports = function getFileType (file) {
+  const fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null
+
+  if (file.isRemote) {
+    // some remote providers do not support file types
+    return file.type ? file.type : mimeTypes[fileExtension]
+  }
+
+  // check if mime type is set in the file object
+  if (file.type) {
+    return file.type
+  }
+
+  // see if we can map extension to a mime type
+  if (fileExtension && mimeTypes[fileExtension]) {
+    return mimeTypes[fileExtension]
+  }
+
+  // if all fails, well, return empty
+  return null
+}

+ 47 - 0
src/utils/getFileType.test.js

@@ -0,0 +1,47 @@
+const getFileType = require('./getFileType')
+
+describe('getFileType', () => {
+  it('should trust the filetype if the file comes from a remote source', () => {
+    const file = {
+      isRemote: true,
+      type: 'audio/webm',
+      name: 'foo.webm'
+    }
+    expect(getFileType(file)).toEqual('audio/webm')
+  })
+
+  it('should determine the filetype from the mimetype', () => {
+    const file = {
+      type: 'audio/webm',
+      name: 'foo.webm',
+      data: 'sdfsdfhq9efbicw'
+    }
+    expect(getFileType(file)).toEqual('audio/webm')
+  })
+
+  it('should determine the filetype from the extension', () => {
+    const fileMP3 = {
+      name: 'foo.mp3',
+      data: 'sdfsfhfh329fhwihs'
+    }
+    const fileYAML = {
+      name: 'bar.yaml',
+      data: 'sdfsfhfh329fhwihs'
+    }
+    const fileMKV = {
+      name: 'bar.mkv',
+      data: 'sdfsfhfh329fhwihs'
+    }
+    expect(getFileType(fileMP3)).toEqual('audio/mp3')
+    expect(getFileType(fileYAML)).toEqual('text/yaml')
+    expect(getFileType(fileMKV)).toEqual('video/x-matroska')
+  })
+
+  it('should fail gracefully if unable to detect', () => {
+    const file = {
+      name: 'foobar',
+      data: 'sdfsfhfh329fhwihs'
+    }
+    expect(getFileType(file)).toEqual(null)
+  })
+})

+ 16 - 0
src/utils/getFileTypeExtension.js

@@ -0,0 +1,16 @@
+// 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'
+}
+
+module.exports = function getFileTypeExtension (mimeType) {
+  return mimeToExtensions[mimeType] || null
+}

+ 13 - 0
src/utils/getFileTypeExtension.test.js

@@ -0,0 +1,13 @@
+const getFileTypeExtension = require('./getFileTypeExtension')
+
+describe('getFileTypeExtension', () => {
+  it('should return the filetype based on the specified mime type', () => {
+    expect(getFileTypeExtension('video/ogg')).toEqual('ogv')
+    expect(getFileTypeExtension('audio/ogg')).toEqual('ogg')
+    expect(getFileTypeExtension('video/webm')).toEqual('webm')
+    expect(getFileTypeExtension('audio/webm')).toEqual('webm')
+    expect(getFileTypeExtension('video/mp4')).toEqual('mp4')
+    expect(getFileTypeExtension('audio/mp3')).toEqual('mp3')
+    expect(getFileTypeExtension('foo/bar')).toEqual(null)
+  })
+})

+ 8 - 0
src/utils/getSocketHost.js

@@ -0,0 +1,8 @@
+module.exports = 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}`
+}

+ 9 - 0
src/utils/getSocketHost.test.js

@@ -0,0 +1,9 @@
+const getSocketHost = require('./getSocketHost')
+
+describe('getSocketHost', () => {
+  it('should get the host from the specified url', () => {
+    expect(
+        getSocketHost('https://foo.bar/a/b/cd?e=fghi&l=k&m=n')
+      ).toEqual('ws://foo.bar/a/b/cd?e=fghi&l=k&m=n')
+  })
+})

+ 7 - 0
src/utils/getSpeed.js

@@ -0,0 +1,7 @@
+module.exports = function getSpeed (fileProgress) {
+  if (!fileProgress.bytesUploaded) return 0
+
+  const timeElapsed = (new Date()) - fileProgress.uploadStarted
+  const uploadSpeed = fileProgress.bytesUploaded / (timeElapsed / 1000)
+  return uploadSpeed
+}

+ 13 - 0
src/utils/getSpeed.test.js

@@ -0,0 +1,13 @@
+const getSpeed = require('./getSpeed')
+
+describe('getSpeed', () => {
+  it('should calculate the speed given a fileProgress object', () => {
+    const dateNow = new Date()
+    const date5SecondsAgo = new Date(dateNow.getTime() - 5 * 1000)
+    const fileProgress = {
+      bytesUploaded: 1024,
+      uploadStarted: date5SecondsAgo
+    }
+    expect(Math.round(getSpeed(fileProgress))).toEqual(Math.round(205))
+  })
+})

+ 17 - 0
src/utils/getTimeStamp.js

@@ -0,0 +1,17 @@
+/**
+ * Returns a timestamp in the format of `hours:minutes:seconds`
+*/
+module.exports = 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
+}

+ 8 - 0
src/utils/isDOMElement.js

@@ -0,0 +1,8 @@
+/**
+ * Check if an object is a DOM element. Duck-typing based on `nodeType`.
+ *
+ * @param {*} obj
+ */
+module.exports = function isDOMElement (obj) {
+  return obj && typeof obj === 'object' && obj.nodeType === Node.ELEMENT_NODE
+}

+ 9 - 0
src/utils/isObjectURL.js

@@ -0,0 +1,9 @@
+/**
+ * Check if a URL string is an object URL from `URL.createObjectURL`.
+ *
+ * @param {string} url
+ * @return {boolean}
+ */
+module.exports = function isObjectURL (url) {
+  return url.indexOf('blob:') === 0
+}

+ 10 - 0
src/utils/isObjectURL.test.js

@@ -0,0 +1,10 @@
+const isObjectURL = require('./isObjectURL')
+
+describe('isObjectURL', () => {
+  it('should return true if the specified url is an object url', () => {
+    expect(isObjectURL('blob:abc123')).toEqual(true)
+    expect(isObjectURL('kblob:abc123')).toEqual(false)
+    expect(isObjectURL('blob-abc123')).toEqual(false)
+    expect(isObjectURL('abc123')).toEqual(false)
+  })
+})

+ 9 - 0
src/utils/isPreviewSupported.js

@@ -0,0 +1,9 @@
+module.exports = function isPreviewSupported (fileType) {
+  if (!fileType) return false
+  const 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
+}

+ 11 - 0
src/utils/isPreviewSupported.test.js

@@ -0,0 +1,11 @@
+const isPreviewSupported = require('./isPreviewSupported')
+
+describe('isPreviewSupported', () => {
+  it('should return true for any filetypes that browsers can preview', () => {
+    const supported = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml', 'image/bmp']
+    supported.forEach(ext => {
+      expect(isPreviewSupported(ext)).toEqual(true)
+    })
+    expect(isPreviewSupported('foo')).toEqual(false)
+  })
+})

+ 4 - 0
src/utils/isTouchDevice.js

@@ -0,0 +1,4 @@
+module.exports = function isTouchDevice () {
+  return 'ontouchstart' in window || // works on most browsers
+          navigator.maxTouchPoints   // works on IE10/11 and Surface
+}

+ 23 - 0
src/utils/isTouchDevice.test.js

@@ -0,0 +1,23 @@
+const isTouchDevice = require('./isTouchDevice')
+
+describe('isTouchDevice', () => {
+  const RealTouchStart = global.window.ontouchstart
+  const RealMaxTouchPoints = global.navigator.maxTouchPoints
+
+  beforeEach(() => {
+    global.window.ontouchstart = true
+    global.navigator.maxTouchPoints = 1
+  })
+
+  afterEach(() => {
+    global.navigator.maxTouchPoints = RealMaxTouchPoints
+    global.window.ontouchstart = RealTouchStart
+  })
+
+  xit("should return true if it's a touch device", () => {
+    expect(isTouchDevice()).toEqual(true)
+    delete global.window.ontouchstart
+    global.navigator.maxTouchPoints = false
+    expect(isTouchDevice()).toEqual(false)
+  })
+})

+ 36 - 0
src/utils/limitPromises.js

@@ -0,0 +1,36 @@
+/**
+ * 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()}
+ */
+module.exports = function limitPromises (limit) {
+  let pending = 0
+  const queue = []
+  return (fn) => {
+    return (...args) => {
+      const call = () => {
+        pending++
+        const promise = fn(...args)
+        promise.then(onfinish, onfinish)
+        return promise
+      }
+
+      if (pending >= limit) {
+        return new Promise((resolve, reject) => {
+          queue.push(() => {
+            call().then(resolve, reject)
+          })
+        })
+      }
+      return call()
+    }
+  }
+  function onfinish () {
+    pending--
+    const next = queue.shift()
+    if (next) next()
+  }
+}

+ 47 - 0
src/utils/limitPromises.test.js

@@ -0,0 +1,47 @@
+const limitPromises = require('./limitPromises')
+
+describe('limitPromises', () => {
+  let pending = 0
+  function fn () {
+    pending++
+    return new Promise((resolve) => setTimeout(resolve, 10))
+        .then(() => pending--)
+  }
+
+  it('should run at most N promises at the same time', () => {
+    const limit = limitPromises(4)
+    const fn2 = limit(fn)
+
+    const result = Promise.all([
+      fn2(), fn2(), fn2(), fn2(),
+      fn2(), fn2(), fn2(), fn2(),
+      fn2(), fn2()
+    ])
+
+    expect(pending).toBe(4)
+    setTimeout(() => {
+      expect(pending).toBe(4)
+    }, 10)
+
+    return result.then(() => {
+      expect(pending).toBe(0)
+    })
+  })
+
+  it('should accept Infinity as limit', () => {
+    const limit = limitPromises(Infinity)
+    const fn2 = limit(fn)
+
+    const result = Promise.all([
+      fn2(), fn2(), fn2(), fn2(),
+      fn2(), fn2(), fn2(), fn2(),
+      fn2(), fn2()
+    ])
+
+    expect(pending).toBe(10)
+
+    return result.then(() => {
+      expect(pending).toBe(0)
+    })
+  })
+})

+ 36 - 0
src/utils/mimeTypes.js

@@ -0,0 +1,36 @@
+module.exports = {
+  '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',
+  'yaml': 'text/yaml',
+  'yml': 'text/yaml',
+  'csv': 'text/csv',
+  'avi': 'video/x-msvideo',
+  'mks': 'video/x-matroska',
+  'mkv': 'video/x-matroska',
+  'mov': 'video/quicktime',
+  'doc': 'application/msword',
+  'docm': 'application/vnd.ms-word.document.macroenabled.12',
+  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+  'dot': 'application/msword',
+  'dotm': 'application/vnd.ms-word.template.macroenabled.12',
+  'dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+  'xla': 'application/vnd.ms-excel',
+  'xlam': 'application/vnd.ms-excel.addin.macroenabled.12',
+  'xlc': 'application/vnd.ms-excel',
+  'xlf': 'application/x-xliff+xml',
+  'xlm': 'application/vnd.ms-excel',
+  'xls': 'application/vnd.ms-excel',
+  'xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
+  'xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12',
+  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+  'xlt': 'application/vnd.ms-excel',
+  'xltm': 'application/vnd.ms-excel.template.macroenabled.12',
+  'xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+  'xlw': 'application/vnd.ms-excel'
+}

+ 16 - 0
src/utils/prettyETA.js

@@ -0,0 +1,16 @@
+const secondsToTime = require('./secondsToTime')
+
+module.exports = 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}`
+}

+ 12 - 0
src/utils/prettyETA.test.js

@@ -0,0 +1,12 @@
+const prettyETA = require('./prettyETA')
+
+describe('prettyETA', () => {
+  it('should convert the specified number of seconds to a pretty ETA', () => {
+    expect(prettyETA(0)).toEqual('0s')
+    expect(prettyETA(1.2)).toEqual('1s')
+    expect(prettyETA(1)).toEqual('1s')
+    expect(prettyETA(103)).toEqual('1m 43s')
+    expect(prettyETA(1034.9)).toEqual('17m 14s')
+    expect(prettyETA(103984.1)).toEqual('4h 53m 04s')
+  })
+})

+ 10 - 0
src/utils/runPromiseSequence.js

@@ -0,0 +1,10 @@
+/**
+ * Runs an array of promise-returning functions in sequence.
+ */
+module.exports = function runPromiseSequence (functions, ...args) {
+  let promise = Promise.resolve()
+  functions.forEach((func) => {
+    promise = promise.then(() => func(...args))
+  })
+  return promise
+}

+ 15 - 0
src/utils/runPromiseSequence.test.js

@@ -0,0 +1,15 @@
+const runPromiseSequence = require('./runPromiseSequence')
+
+describe('runPromiseSequence', () => {
+  it('should run an array of promise-returning functions in sequence', () => {
+    const promiseFn1 = jest.fn().mockReturnValue(Promise.resolve)
+    const promiseFn2 = jest.fn().mockReturnValue(Promise.resolve)
+    const promiseFn3 = jest.fn().mockReturnValue(Promise.resolve)
+    return runPromiseSequence([promiseFn1, promiseFn2, promiseFn3])
+        .then(() => {
+          expect(promiseFn1.mock.calls.length).toEqual(1)
+          expect(promiseFn2.mock.calls.length).toEqual(1)
+          expect(promiseFn3.mock.calls.length).toEqual(1)
+        })
+  })
+})

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
src/utils/sampleImageDataURI.js


+ 7 - 0
src/utils/secondsToTime.js

@@ -0,0 +1,7 @@
+module.exports = 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 }
+}

+ 29 - 0
src/utils/secondsToTime.test.js

@@ -0,0 +1,29 @@
+const secondsToTime = require('./secondsToTime')
+
+describe('secondsToTime', () => {
+  it('converts seconds to an { hours, minutes, seconds } object', () => {
+    expect(secondsToTime(60)).toEqual({
+      hours: 0,
+      minutes: 1,
+      seconds: 0
+    })
+
+    expect(secondsToTime(123)).toEqual({
+      hours: 0,
+      minutes: 2,
+      seconds: 3
+    })
+
+    expect(secondsToTime(1060)).toEqual({
+      hours: 0,
+      minutes: 17,
+      seconds: 40
+    })
+
+    expect(secondsToTime(123453460)).toEqual({
+      hours: 20,
+      minutes: 37,
+      seconds: 40
+    })
+  })
+})

+ 21 - 0
src/utils/settle.js

@@ -0,0 +1,21 @@
+module.exports = 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(() => {
+    return {
+      successful: resolutions,
+      failed: rejections
+    }
+  })
+}

+ 28 - 0
src/utils/settle.test.js

@@ -0,0 +1,28 @@
+const settle = require('./settle')
+
+describe('settle', () => {
+  it('should resolve even if all input promises reject', () => {
+    return expect(
+        settle([
+          Promise.reject(new Error('oops')),
+          Promise.reject(new Error('this went wrong'))
+        ])
+      ).resolves.toMatchObject({
+        successful: [],
+        failed: [ new Error('oops'), new Error('this went wrong') ]
+      })
+  })
+
+  it('should resolve with an object if some input promises resolve', () => {
+    return expect(
+        settle([
+          Promise.reject(new Error('rejected')),
+          Promise.resolve('resolved'),
+          Promise.resolve('also-resolved')
+        ])
+      ).resolves.toMatchObject({
+        successful: ['resolved', 'also-resolved'],
+        failed: [new Error('rejected')]
+      })
+  })
+})

+ 6 - 0
src/utils/toArray.js

@@ -0,0 +1,6 @@
+/**
+ * Converts list into array
+*/
+module.exports = function toArray (list) {
+  return Array.prototype.slice.call(list || [], 0)
+}

+ 22 - 0
src/utils/toArray.test.js

@@ -0,0 +1,22 @@
+const toArray = require('./toArray')
+
+describe('toArray', () => {
+  it('should convert a array-like object into an array', () => {
+    const obj = {
+      '0': 'zero',
+      '1': 'one',
+      '2': 'two',
+      '3': 'three',
+      '4': 'four',
+      length: 5
+    }
+
+    expect(toArray(obj)).toEqual([
+      'zero',
+      'one',
+      'two',
+      'three',
+      'four'
+    ])
+  })
+})

+ 9 - 0
src/utils/truncateString.js

@@ -0,0 +1,9 @@
+module.exports = 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
+}

+ 16 - 0
src/utils/truncateString.test.js

@@ -0,0 +1,16 @@
+const truncateString = require('./truncateString')
+
+describe('truncateString', () => {
+  it('should truncate the string by the specified amount', () => {
+    expect(truncateString('abcdefghijkl', 10)).toEqual('abcde...jkl')
+    expect(truncateString('abcdefghijkl', 9)).toEqual('abcd...jkl')
+    expect(truncateString('abcdefghijkl', 8)).toEqual('abcd...kl')
+    expect(truncateString('abcdefghijkl', 7)).toEqual('abc...kl')
+    expect(truncateString('abcdefghijkl', 6)).toEqual('abc...kl')
+    expect(truncateString('abcdefghijkl', 5)).toEqual('ab...kl')
+    expect(truncateString('abcdefghijkl', 4)).toEqual('ab...l')
+    expect(truncateString('abcdefghijkl', 3)).toEqual('a...l')
+    expect(truncateString('abcdefghijkl', 2)).toEqual('a...l')
+    expect(truncateString('abcdefghijkl', 1)).toEqual('...l')
+  })
+})

+ 7 - 5
src/views/ProviderView/index.js

@@ -1,8 +1,10 @@
+const { h, Component } = require('preact')
 const AuthView = require('./AuthView')
 const Browser = require('./Browser')
 const LoaderView = require('./Loader')
-const Utils = require('../../core/Utils')
-const { h, Component } = require('preact')
+const generateFileID = require('../../utils/generateFileID')
+const getFileType = require('../../utils/getFileType')
+const isPreviewSupported = require('../../utils/isPreviewSupported')
 
 /**
  * Array.prototype.findIndex ponyfill for old browsers.
@@ -191,9 +193,9 @@ module.exports = class ProviderView {
       }
     }
 
-    const fileType = Utils.getFileType(tagFile)
+    const fileType = getFileType(tagFile)
     // TODO Should we just always use the thumbnail URL if it exists?
-    if (fileType && Utils.isPreviewSupported(fileType)) {
+    if (fileType && isPreviewSupported(fileType)) {
       tagFile.preview = this.plugin.getItemThumbnailUrl(file)
     }
     this.plugin.uppy.log('Adding remote file')
@@ -432,7 +434,7 @@ module.exports = class ProviderView {
   }
 
   providerFileToId (file) {
-    return Utils.generateFileID({
+    return generateFileID({
       data: this.plugin.getItemData(file),
       name: this.plugin.getItemName(file) || this.plugin.getItemId(file),
       type: this.plugin.getMimeType(file)

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott