PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/src/dropzone.coffee

https://github.com/baldbunny/dropzone
CoffeeScript | 1131 lines | 732 code | 215 blank | 184 comment | 133 complexity | 42b2222ac06f7d372cb6a3daed124898 MD5 | raw file
  1. ###
  2. #
  3. # More info at [www.dropzonejs.com](http://www.dropzonejs.com)
  4. #
  5. # Copyright (c) 2012, Matias Meno
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###
  26. # Dependencies
  27. Em = Emitter ? require "emitter" # Can't be the same name because it will lead to a local variable
  28. noop = ->
  29. class Dropzone extends Em
  30. ###
  31. This is a list of all available events you can register on a dropzone object.
  32. You can register an event handler like this:
  33. dropzone.on("dragEnter", function() { });
  34. ###
  35. events: [
  36. "drop"
  37. "dragstart"
  38. "dragend"
  39. "dragenter"
  40. "dragover"
  41. "dragleave"
  42. "selectedfiles"
  43. "addedfile"
  44. "removedfile"
  45. "thumbnail"
  46. "error"
  47. "processingfile"
  48. "uploadprogress"
  49. "totaluploadprogress"
  50. "sending"
  51. "success"
  52. "complete"
  53. "reset"
  54. ]
  55. defaultOptions:
  56. url: null
  57. method: "post"
  58. withCredentials: no
  59. parallelUploads: 2
  60. maxFilesize: 256 # in MB
  61. paramName: "file" # The name of the file param that gets transferred.
  62. createImageThumbnails: true
  63. maxThumbnailFilesize: 10 # in MB. When the filename exceeds this limit, the thumbnail will not be generated.
  64. thumbnailWidth: 100
  65. thumbnailHeight: 100
  66. # Can be an object of additional parameters to transfer to the server.
  67. # This is the same as adding hidden input fields in the form element.
  68. params: { }
  69. # If true, the dropzone will present a file selector when clicked.
  70. clickable: yes
  71. # You can set accepted mime types here.
  72. #
  73. # The default implementation of the `accept()` function will check this
  74. # property, and if the Dropzone is clickable this will be used as
  75. # `accept` attribute.
  76. #
  77. # See https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept
  78. # for a reference.
  79. acceptedMimeTypes: null # eg: "audio/*,video/*,image/*"
  80. # @deprecated
  81. # Use acceptedMimeTypes instead.
  82. acceptParameter: null
  83. # If false, files will not be added to the process queue automatically.
  84. # This can be useful if you need some additional user input before sending
  85. # files.
  86. # If you're ready to send the file, set the file.status to Dropzone.QUEUED
  87. # and call processQueue()
  88. enqueueForUpload: yes
  89. # If true, Dropzone will add a link to each file preview to cancel/remove
  90. # the upload.
  91. # See dictCancelUpload and dictRemoveFile to use different words.
  92. addRemoveLinks: no
  93. # A CSS selector or HTML element for the file previews container.
  94. # If null, the dropzone element itself will be used
  95. previewsContainer: null
  96. # Dictionary
  97. # The text used before any files are dropped
  98. dictDefaultMessage: "Drop files here to upload"
  99. # The text that replaces the default message text it the browser is not supported
  100. dictFallbackMessage: "Your browser does not support drag'n'drop file uploads."
  101. # The text that will be added before the fallback form
  102. # If null, no text will be added at all.
  103. dictFallbackText: "Please use the fallback form below to upload your files like in the olden days."
  104. # If the filesize is too big.
  105. dictFileTooBig: "File is too big ({{filesize}}MB). Max filesize: {{maxFilesize}}MB."
  106. # If the file doesn't match the file type.
  107. dictInvalidFileType: "You can't upload files of this type."
  108. # If the server response was invalid.
  109. dictResponseError: "Server responded with {{statusCode}} code."
  110. # If used, the text to be used for the cancel upload link.
  111. dictCancelUpload: "Cancel upload"
  112. # If used, the text to be used for confirmation when cancelling upload.
  113. dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?"
  114. # If used, the text to be used to remove a file.
  115. dictRemoveFile: "Remove file"
  116. # If `done()` is called without argument the file is accepted
  117. # If you call it with an error message, the file is rejected
  118. # (This allows for asynchronous validation).
  119. accept: (file, done) -> done()
  120. # Called when dropzone initialized
  121. # You can add event listeners here
  122. init: -> noop
  123. # Used to debug dropzone and force the fallback form.
  124. forceFallback: off
  125. # Called when the browser does not support drag and drop
  126. fallback: ->
  127. # This code should pass in IE7... :(
  128. @element.className = "#{@element.className} dz-browser-not-supported"
  129. for child in @element.getElementsByTagName "div"
  130. if /(^| )dz-message($| )/.test child.className
  131. messageElement = child
  132. child.className = "dz-message" # Removes the 'dz-default' class
  133. continue
  134. unless messageElement
  135. messageElement = Dropzone.createElement """<div class="dz-message"><span></span></div>"""
  136. @element.appendChild messageElement
  137. span = messageElement.getElementsByTagName("span")[0]
  138. span.textContent = @options.dictFallbackMessage if span
  139. @element.appendChild @getFallbackForm()
  140. # Gets called to calculate the thumbnail dimensions.
  141. #
  142. # You can use file.width, file.height, options.thumbnailWidth and
  143. # options.thumbnailHeight to calculate the dimensions.
  144. #
  145. # The dimensions are going to be used like this:
  146. #
  147. # var info = @options.resize.call(this, file);
  148. # ctx.drawImage(img, info.srcX, info.srcY, info.srcWidth, info.srcHeight, info.trgX, info.trgY, info.trgWidth, info.trgHeight);
  149. #
  150. # srcX, srcy, trgX and trgY can be omitted (in which case 0 is assumed).
  151. # trgWidth and trgHeight can be omitted (in which case the options.thumbnailWidth / options.thumbnailHeight are used)
  152. resize: (file) ->
  153. info =
  154. srcX: 0
  155. srcY: 0
  156. srcWidth: file.width
  157. srcHeight: file.height
  158. srcRatio = file.width / file.height
  159. trgRatio = @options.thumbnailWidth / @options.thumbnailHeight
  160. if file.height < @options.thumbnailHeight or file.width < @options.thumbnailWidth
  161. # This image is smaller than the canvas
  162. info.trgHeight = info.srcHeight
  163. info.trgWidth = info.srcWidth
  164. else
  165. # Image is bigger and needs rescaling
  166. if srcRatio > trgRatio
  167. info.srcHeight = file.height
  168. info.srcWidth = info.srcHeight * trgRatio
  169. else
  170. info.srcWidth = file.width
  171. info.srcHeight = info.srcWidth / trgRatio
  172. info.srcX = (file.width - info.srcWidth) / 2
  173. info.srcY = (file.height - info.srcHeight) / 2
  174. return info
  175. ###
  176. Those functions register themselves to the events on init and handle all
  177. the user interface specific stuff. Overwriting them won't break the upload
  178. but can break the way it's displayed.
  179. You can overwrite them if you don't like the default behavior. If you just
  180. want to add an additional event handler, register it on the dropzone object
  181. and don't overwrite those options.
  182. ###
  183. # Those are self explanatory and simply concern the DragnDrop.
  184. drop: (e) -> @element.classList.remove "dz-drag-hover"
  185. dragstart: noop
  186. dragend: (e) -> @element.classList.remove "dz-drag-hover"
  187. dragenter: (e) -> @element.classList.add "dz-drag-hover"
  188. dragover: (e) -> @element.classList.add "dz-drag-hover"
  189. dragleave: (e) -> @element.classList.remove "dz-drag-hover"
  190. # Called whenever files are dropped or selected
  191. selectedfiles: (files) ->
  192. @element.classList.add "dz-started" if @element == @previewsContainer
  193. # Called whenever there are no files left in the dropzone anymore, and the
  194. # dropzone should be displayed as if in the initial state.
  195. reset: ->
  196. @element.classList.remove "dz-started"
  197. # Called when a file is added to the queue
  198. # Receives `file`
  199. addedfile: (file) ->
  200. file.previewElement = Dropzone.createElement @options.previewTemplate
  201. file.previewTemplate = file.previewElement # Backwards compatibility
  202. @previewsContainer.appendChild file.previewElement
  203. file.previewElement.querySelector("[data-dz-name]").textContent = file.name
  204. file.previewElement.querySelector("[data-dz-size]").innerHTML = @filesize file.size
  205. if @options.addRemoveLinks
  206. file._removeLink = Dropzone.createElement """<a class="dz-remove" href="javascript:undefined;">#{@options.dictRemoveFile}</a>"""
  207. file._removeLink.addEventListener "click", (e) =>
  208. e.preventDefault()
  209. e.stopPropagation()
  210. if file.status == Dropzone.UPLOADING
  211. @removeFile file if window.confirm @options.dictCancelUploadConfirmation
  212. else
  213. @removeFile file
  214. file.previewElement.appendChild file._removeLink
  215. # Called whenever a file is removed.
  216. removedfile: (file) ->
  217. file.previewElement?.parentNode.removeChild file.previewElement
  218. # Called when a thumbnail has been generated
  219. # Receives `file` and `dataUrl`
  220. thumbnail: (file, dataUrl) ->
  221. file.previewElement.classList.remove "dz-file-preview"
  222. file.previewElement.classList.add "dz-image-preview"
  223. thumbnailElement = file.previewElement.querySelector("[data-dz-thumbnail]")
  224. thumbnailElement.alt = file.name
  225. thumbnailElement.src = dataUrl
  226. # Called whenever an error occurs
  227. # Receives `file` and `message`
  228. error: (file, message) ->
  229. file.previewElement.classList.add "dz-error"
  230. file.previewElement.querySelector("[data-dz-errormessage]").textContent = message
  231. # Called when a file gets processed. Since there is a cue, not all added
  232. # files are processed immediately.
  233. # Receives `file`
  234. processingfile: (file) ->
  235. file.previewElement.classList.add "dz-processing"
  236. file._removeLink.textContent = @options.dictCancelUpload if file._removeLink
  237. # Called whenever the upload progress gets updated.
  238. # Receives `file`, `progress` (percentage 0-100) and `bytesSent`.
  239. # To get the total number of bytes of the file, use `file.size`
  240. uploadprogress: (file, progress, bytesSent) ->
  241. file.previewElement.querySelector("[data-dz-uploadprogress]").style.width = "#{progress}%"
  242. # Called whenever the total upload progress gets updated.
  243. # Called with totalUploadProgress (0-100), totalBytes and totalBytesSent
  244. totaluploadprogress: noop
  245. # Called just before the file is sent. Gets the `xhr` object as second
  246. # parameter, so you can modify it (for example to add a CSRF token) and a
  247. # `formData` object to add additional information.
  248. sending: noop
  249. # When the complete upload is finished and successfull
  250. # Receives `file`
  251. success: (file) ->
  252. file.previewElement.classList.add "dz-success"
  253. # When the upload is finished, either with success or an error.
  254. # Receives `file`
  255. complete: (file) ->
  256. file._removeLink.textContent = @options.dictRemoveFile if file._removeLink
  257. # This template will be chosen when a new file is dropped.
  258. previewTemplate: """
  259. <div class="dz-preview dz-file-preview">
  260. <div class="dz-details">
  261. <div class="dz-filename"><span data-dz-name></span></div>
  262. <div class="dz-size" data-dz-size></div>
  263. <img data-dz-thumbnail />
  264. </div>
  265. <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
  266. <div class="dz-success-mark"><span>✔</span></div>
  267. <div class="dz-error-mark"><span>✘</span></div>
  268. <div class="dz-error-message"><span data-dz-errormessage></span></div>
  269. </div>
  270. """
  271. # global utility
  272. extend = (target, objects...) ->
  273. for object in objects
  274. target[key] = val for key, val of object
  275. target
  276. constructor: (@element, options) ->
  277. # For backwards compatibility since the version was in the prototype previously
  278. @version = Dropzone.version
  279. @defaultOptions.previewTemplate = @defaultOptions.previewTemplate.replace /\n*/g, ""
  280. @clickableElements = [ ]
  281. @listeners = [ ]
  282. @files = [] # All files
  283. @element = document.querySelector @element if typeof @element == "string"
  284. # Not checking if instance of HTMLElement or Element since IE9 is extremely weird.
  285. throw new Error "Invalid dropzone element." unless @element and @element.nodeType?
  286. throw new Error "Dropzone already attached." if @element.dropzone
  287. # Now add this dropzone to the instances.
  288. Dropzone.instances.push @
  289. # Put the dropzone inside the element itself.
  290. element.dropzone = @
  291. elementOptions = Dropzone.optionsForElement(@element) ? { }
  292. @options = extend { }, @defaultOptions, elementOptions, options ? { }
  293. @options.url = @element.action unless @options.url?
  294. throw new Error "No URL provided." unless @options.url
  295. throw new Error "You can't provide both 'acceptParameter' and 'acceptedMimeTypes'. 'acceptParameter' is deprecated." if @options.acceptParameter and @options.acceptedMimeTypes
  296. @options.method = @options.method.toUpperCase()
  297. # If the browser failed, just call the fallback and leave
  298. return @options.fallback.call this if @options.forceFallback or !Dropzone.isBrowserSupported()
  299. if (fallback = @getExistingFallback()) and fallback.parentNode
  300. # Remove the fallback
  301. fallback.parentNode.removeChild fallback
  302. if @options.previewsContainer
  303. @previewsContainer = Dropzone.getElement @options.previewsContainer, "previewsContainer"
  304. else
  305. @previewsContainer = @element
  306. if @options.clickable
  307. if @options.clickable == yes
  308. @clickableElements = [ @element ]
  309. else
  310. @clickableElements = Dropzone.getElements @options.clickable, "clickable"
  311. @init()
  312. # Returns all files that have been accepted
  313. getAcceptedFiles: -> file for file in @files when file.accepted
  314. # Returns all files that have been rejected
  315. # Not sure when that's going to be useful, but added for completeness.
  316. getRejectedFiles: -> file for file in @files unless file.accepted
  317. # Returns all files that are in the queue
  318. getQueuedFiles: -> file for file in @files when file.status == Dropzone.QUEUED
  319. getUploadingFiles: -> file for file in @files when file.status == Dropzone.UPLOADING
  320. enqueueFile: (file) ->
  321. if file.status == Dropzone.ACCEPTED
  322. file.status = Dropzone.QUEUED
  323. @processQueue()
  324. else
  325. throw new Error "This file can't be queued because it has already been processed or was rejected."
  326. init: ->
  327. # In case it isn't set already
  328. @element.setAttribute("enctype", "multipart/form-data") if @element.tagName == "form"
  329. if @element.classList.contains("dropzone") and !@element.querySelector(".dz-message")
  330. @element.appendChild Dropzone.createElement """<div class="dz-default dz-message"><span>#{@options.dictDefaultMessage}</span></div>"""
  331. if @clickableElements.length
  332. setupHiddenFileInput = =>
  333. document.body.removeChild @hiddenFileInput if @hiddenFileInput
  334. @hiddenFileInput = document.createElement "input"
  335. @hiddenFileInput.setAttribute "type", "file"
  336. @hiddenFileInput.setAttribute "multiple", "multiple"
  337. @hiddenFileInput.setAttribute "accept", @options.acceptedMimeTypes if @options.acceptedMimeTypes?
  338. # Backwards compatibility
  339. @hiddenFileInput.setAttribute "accept", @options.acceptParameter if @options.acceptParameter?
  340. # Not setting `display="none"` because some browsers don't accept clicks
  341. # on elements that aren't displayed.
  342. @hiddenFileInput.style.visibility = "hidden"
  343. @hiddenFileInput.style.position = "absolute"
  344. @hiddenFileInput.style.top = "0"
  345. @hiddenFileInput.style.left = "0"
  346. @hiddenFileInput.style.height = "0"
  347. @hiddenFileInput.style.width = "0"
  348. document.body.appendChild @hiddenFileInput
  349. @hiddenFileInput.addEventListener "change", =>
  350. files = @hiddenFileInput.files
  351. if files.length
  352. @emit "selectedfiles", files
  353. @handleFiles files
  354. setupHiddenFileInput()
  355. setupHiddenFileInput()
  356. @URL = window.URL ? window.webkitURL
  357. # Setup all event listeners on the Dropzone object itself.
  358. # They're not in @setupEventListeners() because they shouldn't be removed
  359. # again when the dropzone gets disabled.
  360. @on eventName, @options[eventName] for eventName in @events
  361. @on "uploadprogress", => @updateTotalUploadProgress()
  362. @on "removedfile", => @updateTotalUploadProgress()
  363. noPropagation = (e) ->
  364. e.stopPropagation()
  365. if e.preventDefault
  366. e.preventDefault()
  367. else
  368. e.returnValue = false
  369. # Create the listeners
  370. @listeners = [
  371. {
  372. element: @element
  373. events:
  374. "dragstart": (e) =>
  375. @emit "dragstart", e
  376. "dragenter": (e) =>
  377. noPropagation e
  378. @emit "dragenter", e
  379. "dragover": (e) =>
  380. noPropagation e
  381. @emit "dragover", e
  382. "dragleave": (e) =>
  383. @emit "dragleave", e
  384. "drop": (e) =>
  385. noPropagation e
  386. @drop e
  387. @emit "drop", e
  388. "dragend": (e) =>
  389. @emit "dragend", e
  390. }
  391. ]
  392. @clickableElements.forEach (clickableElement) =>
  393. @listeners.push
  394. element: clickableElement
  395. events:
  396. "click": (evt) =>
  397. # Only the actual dropzone or the message element should trigger file selection
  398. if (clickableElement != @element) or (evt.target == @element or Dropzone.elementInside evt.target, @element.querySelector ".dz-message")
  399. @hiddenFileInput.click() # Forward the click
  400. @enable()
  401. @options.init.call @
  402. # Not fully tested yet
  403. destroy: ->
  404. @disable()
  405. @removeAllFiles true
  406. if @hiddenFileInput?.parentNode
  407. @hiddenFileInput.parentNode.removeChild @hiddenFileInput
  408. @hiddenFileInput = null
  409. updateTotalUploadProgress: ->
  410. totalBytesSent = 0
  411. totalBytes = 0
  412. acceptedFiles = @getAcceptedFiles()
  413. if acceptedFiles.length
  414. for file in @getAcceptedFiles()
  415. totalBytesSent += file.upload.bytesSent
  416. totalBytes += file.upload.total
  417. totalUploadProgress = 100 * totalBytesSent / totalBytes
  418. else
  419. totalUploadProgress = 100
  420. @emit "totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent
  421. # Returns a form that can be used as fallback if the browser does not support DragnDrop
  422. #
  423. # If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided.
  424. # This code has to pass in IE7 :(
  425. getFallbackForm: ->
  426. return existingFallback if existingFallback = @getExistingFallback()
  427. fieldsString = """<div class="dz-fallback">"""
  428. fieldsString += """<p>#{@options.dictFallbackText}</p>""" if @options.dictFallbackText
  429. fieldsString += """<input type="file" name="#{@options.paramName}[]" multiple="multiple" /><button type="submit">Upload!</button></div>"""
  430. fields = Dropzone.createElement fieldsString
  431. if @element.tagName isnt "FORM"
  432. form = Dropzone.createElement("""<form action="#{@options.url}" enctype="multipart/form-data" method="#{@options.method}"></form>""")
  433. form.appendChild fields
  434. else
  435. # Make sure that the enctype and method attributes are set properly
  436. @element.setAttribute "enctype", "multipart/form-data"
  437. @element.setAttribute "method", @options.method
  438. form ? fields
  439. # Returns the fallback elements if they exist already
  440. #
  441. # This code has to pass in IE7 :(
  442. getExistingFallback: ->
  443. getFallback = (elements) -> return el for el in elements when /(^| )fallback($| )/.test el.className
  444. for tagName in [ "div", "form" ]
  445. return fallback if fallback = getFallback @element.getElementsByTagName tagName
  446. # Activates all listeners stored in @listeners
  447. setupEventListeners: ->
  448. for elementListeners in @listeners
  449. elementListeners.element.addEventListener event, listener, false for event, listener of elementListeners.events
  450. # Deactivates all listeners stored in @listeners
  451. removeEventListeners: ->
  452. for elementListeners in @listeners
  453. elementListeners.element.removeEventListener event, listener, false for event, listener of elementListeners.events
  454. # Removes all event listeners and cancels all files in the queue or being processed.
  455. disable: ->
  456. @clickableElements.forEach (element) -> element.classList.remove "dz-clickable"
  457. @removeEventListeners()
  458. @cancelUpload file for file in @files
  459. enable: ->
  460. @clickableElements.forEach (element) -> element.classList.add "dz-clickable"
  461. @setupEventListeners()
  462. # Returns a nicely formatted filesize
  463. filesize: (size) ->
  464. if size >= 100000000000
  465. size = size / 100000000000
  466. string = "TB"
  467. else if size >= 100000000
  468. size = size / 100000000
  469. string = "GB"
  470. else if size >= 100000
  471. size = size / 100000
  472. string = "MB"
  473. else if size >= 100
  474. size = size / 100
  475. string = "KB"
  476. else
  477. size = size * 10
  478. string = "b"
  479. "<strong>#{Math.round(size)/10}</strong> #{string}"
  480. drop: (e) ->
  481. return unless e.dataTransfer
  482. files = e.dataTransfer.files
  483. @emit "selectedfiles", files
  484. @handleFiles files if files.length
  485. handleFiles: (files) ->
  486. @addFile file for file in files
  487. # If `done()` is called without argument the file is accepted
  488. # If you call it with an error message, the file is rejected
  489. # (This allows for asynchronous validation)
  490. #
  491. # This function checks the filesize, and if the file.type passes the
  492. # `acceptedMimeTypes` check.
  493. accept: (file, done) ->
  494. if file.size > @options.maxFilesize * 1024 * 1024
  495. done @options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", @options.maxFilesize)
  496. else unless Dropzone.isValidMimeType file.type, @options.acceptedMimeTypes
  497. done @options.dictInvalidFileType
  498. else
  499. @options.accept.call this, file, done
  500. addFile: (file) ->
  501. file.upload =
  502. progress: 0
  503. # Setting the total upload size to file.size for the beginning
  504. # It's actual different than the size to be transmitted.
  505. total: file.size
  506. bytesSent: 0
  507. @files.push file
  508. file.status = Dropzone.ADDED
  509. @emit "addedfile", file
  510. @createThumbnail file if @options.createImageThumbnails and file.type.match(/image.*/) and file.size <= @options.maxThumbnailFilesize * 1024 * 1024
  511. @accept file, (error) =>
  512. if error
  513. file.accepted = false # Backwards compatibility
  514. @errorProcessing file, error # Will set the file.status
  515. else
  516. file.status = Dropzone.ACCEPTED
  517. file.accepted = true # Backwards compatibility
  518. if @options.enqueueForUpload
  519. file.status = Dropzone.QUEUED
  520. @processQueue()
  521. # Can be called by the user to remove a file
  522. removeFile: (file) ->
  523. @cancelUpload file if file.status == Dropzone.UPLOADING
  524. @files = without @files, file
  525. @emit "removedfile", file
  526. @emit "reset" if @files.length == 0
  527. # Removes all files that aren't currently processed from the list
  528. removeAllFiles: (cancelIfNecessary = off) ->
  529. # Create a copy of files since removeFile() changes the @files array.
  530. for file in @files.slice()
  531. @removeFile file if file.status != Dropzone.UPLOADING || cancelIfNecessary
  532. return null
  533. createThumbnail: (file) ->
  534. fileReader = new FileReader
  535. fileReader.onload = =>
  536. img = new Image
  537. img.onload = =>
  538. file.width = img.width
  539. file.height = img.height
  540. resizeInfo = @options.resize.call @, file
  541. resizeInfo.trgWidth ?= @options.thumbnailWidth
  542. resizeInfo.trgHeight ?= @options.thumbnailHeight
  543. canvas = document.createElement "canvas"
  544. ctx = canvas.getContext "2d"
  545. canvas.width = resizeInfo.trgWidth
  546. canvas.height = resizeInfo.trgHeight
  547. ctx.drawImage img, resizeInfo.srcX ? 0, resizeInfo.srcY ? 0, resizeInfo.srcWidth, resizeInfo.srcHeight, resizeInfo.trgX ? 0, resizeInfo.trgY ? 0, resizeInfo.trgWidth, resizeInfo.trgHeight
  548. thumbnail = canvas.toDataURL "image/png"
  549. @emit "thumbnail", file, thumbnail
  550. img.src = fileReader.result
  551. fileReader.readAsDataURL file
  552. # Goes through the queue and processes files if there aren't too many already.
  553. processQueue: ->
  554. parallelUploads = @options.parallelUploads
  555. processingLength = @getUploadingFiles().length
  556. i = processingLength
  557. queuedFiles = @getQueuedFiles()
  558. while i < parallelUploads
  559. return unless queuedFiles.length # Nothing left to process
  560. @processFile queuedFiles.shift()
  561. i++
  562. # Loads the file, then calls finishedLoading()
  563. processFile: (file) ->
  564. file.processing = yes # Backwards compatibility
  565. file.status = Dropzone.UPLOADING
  566. @emit "processingfile", file
  567. @uploadFile file
  568. # Cancels the file upload and sets the status to CANCELED
  569. # **if** the file is actually being uploaded.
  570. # If it's still in the queue, the file is being removed from it and the status
  571. # set to CANCELED.
  572. cancelUpload: (file) ->
  573. if file.status == Dropzone.UPLOADING
  574. file.status = Dropzone.CANCELED
  575. file.xhr.abort()
  576. else if file.status in [ Dropzone.ADDED, Dropzone.ACCEPTED, Dropzone.QUEUED ]
  577. file.status = Dropzone.CANCELED
  578. @emit "complete", file
  579. @processQueue()
  580. uploadFile: (file) ->
  581. xhr = new XMLHttpRequest()
  582. # Put the xhr object in the file object to be able to reference it later.
  583. file.xhr = xhr
  584. xhr.withCredentials = !!@options.withCredentials
  585. xhr.open @options.method, @options.url, true
  586. response = null
  587. handleError = =>
  588. @errorProcessing file, response || @options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr
  589. updateProgress = (e) =>
  590. if e?
  591. progress = 100 * e.loaded / e.total
  592. file.upload =
  593. progress: progress
  594. total: e.total
  595. bytesSent: e.loaded
  596. else
  597. # Called when the file finished uploading
  598. # Nothing to do, already at 100%
  599. return if file.upload.progress == 100 and file.upload.bytesSent == file.upload.total
  600. progress = 100
  601. file.upload.progress = progress
  602. file.upload.bytesSent = file.upload.total
  603. @emit "uploadprogress", file, progress, file.upload.bytesSent
  604. xhr.onload = (e) =>
  605. return if file.status == Dropzone.CANCELED
  606. return unless xhr.readyState is 4
  607. response = xhr.responseText
  608. if xhr.getResponseHeader("content-type") and ~xhr.getResponseHeader("content-type").indexOf "application/json"
  609. try
  610. response = JSON.parse response
  611. catch e
  612. response = "Invalid JSON response from server."
  613. updateProgress()
  614. unless 200 <= xhr.status < 300
  615. handleError()
  616. else
  617. @finished file, response, e
  618. xhr.onerror = =>
  619. return if file.status == Dropzone.CANCELED
  620. handleError()
  621. # Some browsers do not have the .upload property
  622. progressObj = xhr.upload ? xhr
  623. progressObj.onprogress = updateProgress
  624. headers =
  625. "Accept": "application/json",
  626. "Cache-Control": "no-cache",
  627. "X-Requested-With": "XMLHttpRequest",
  628. "X-File-Name": encodeURIComponent file.name
  629. extend headers, @options.headers if @options.headers
  630. xhr.setRequestHeader header, name for header, name of headers
  631. formData = new FormData()
  632. # Adding all @options parameters
  633. formData.append key, value for key, value of @options.params if @options.params
  634. # Take care of other input elements
  635. if @element.tagName == "FORM"
  636. for input in @element.querySelectorAll "input, textarea, select, button"
  637. inputName = input.getAttribute "name"
  638. inputType = input.getAttribute "type"
  639. if !inputType or inputType.toLowerCase() != "checkbox" or input.checked
  640. formData.append inputName, input.value
  641. # Let the user add additional data if necessary
  642. @emit "sending", file, xhr, formData
  643. # Finally add the file
  644. # Has to be last because some servers (eg: S3) expect the file to be the
  645. # last parameter
  646. formData.append @options.paramName, file
  647. xhr.send formData
  648. # Called internally when processing is finished.
  649. # Individual callbacks have to be called in the appropriate sections.
  650. finished: (file, responseText, e) ->
  651. file.status = Dropzone.SUCCESS
  652. @processQueue()
  653. @emit "success", file, responseText, e
  654. @emit "finished", file, responseText, e # For backwards compatibility
  655. @emit "complete", file
  656. # Called internally when processing is finished.
  657. # Individual callbacks have to be called in the appropriate sections.
  658. errorProcessing: (file, message, xhr) ->
  659. file.status = Dropzone.ERROR
  660. @processQueue()
  661. @emit "error", file, message, xhr
  662. @emit "complete", file
  663. Dropzone.version = "3.5.1"
  664. # This is a map of options for your different dropzones. Add configurations
  665. # to this object for your different dropzone elemens.
  666. #
  667. # Example:
  668. #
  669. # Dropzone.options.myDropzoneElementId = { maxFilesize: 1 };
  670. #
  671. # To disable autoDiscover for a specific element, you can set `false` as an option:
  672. #
  673. # Dropzone.options.myDisabledElementId = false;
  674. #
  675. # And in html:
  676. #
  677. # <form action="/upload" id="my-dropzone-element-id" class="dropzone"></form>
  678. Dropzone.options = { }
  679. # Returns the options for an element or undefined if none available.
  680. Dropzone.optionsForElement = (element) ->
  681. # Get the `Dropzone.options.elementId` for this element if it exists
  682. if element.id then Dropzone.options[camelize element.id] else undefined
  683. # Holds a list of all dropzone instances
  684. Dropzone.instances = [ ]
  685. # Returns the dropzone for given element if any
  686. Dropzone.forElement = (element) ->
  687. element = document.querySelector element if typeof element == "string"
  688. throw new Error "No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone." unless element?.dropzone?
  689. return element.dropzone
  690. # Set to false if you don't want Dropzone to automatically find and attach to .dropzone elements.
  691. Dropzone.autoDiscover = on
  692. # Looks for all .dropzone elements and creates a dropzone for them
  693. Dropzone.discover = ->
  694. return unless Dropzone.autoDiscover
  695. if document.querySelectorAll
  696. dropzones = document.querySelectorAll ".dropzone"
  697. else
  698. dropzones = [ ]
  699. # IE :(
  700. checkElements = (elements) ->
  701. for el in elements
  702. dropzones.push el if /(^| )dropzone($| )/.test el.className
  703. checkElements document.getElementsByTagName "div"
  704. checkElements document.getElementsByTagName "form"
  705. for dropzone in dropzones
  706. # Create a dropzone unless auto discover has been disabled for specific element
  707. new Dropzone dropzone unless Dropzone.optionsForElement(dropzone) == false
  708. # Since the whole Drag'n'Drop API is pretty new, some browsers implement it,
  709. # but not correctly.
  710. # So I created a blacklist of userAgents. Yes, yes. Browser sniffing, I know.
  711. # But what to do when browsers *theoretically* support an API, but crash
  712. # when using it.
  713. #
  714. # This is a list of regular expressions tested against navigator.userAgent
  715. #
  716. # ** It should only be used on browser that *do* support the API, but
  717. # incorrectly **
  718. #
  719. Dropzone.blacklistedBrowsers = [
  720. # The mac os version of opera 12 seems to have a problem with the File drag'n'drop API.
  721. /opera.*Macintosh.*version\/12/i
  722. # /MSIE\ 10/i
  723. ]
  724. # Checks if the browser is supported
  725. Dropzone.isBrowserSupported = ->
  726. capableBrowser = yes
  727. if window.File and window.FileReader and window.FileList and window.Blob and window.FormData and document.querySelector
  728. unless "classList" of document.createElement "a"
  729. capableBrowser = no
  730. else
  731. # The browser supports the API, but may be blacklisted.
  732. for regex in Dropzone.blacklistedBrowsers
  733. if regex.test navigator.userAgent
  734. capableBrowser = no
  735. continue
  736. else
  737. capableBrowser = no
  738. capableBrowser
  739. # Returns an array without the rejected item
  740. without = (list, rejectedItem) -> item for item in list when item isnt rejectedItem
  741. # abc-def_ghi -> abcDefGhi
  742. camelize = (str) -> str.replace /[\-_](\w)/g, (match) -> match[1].toUpperCase()
  743. # Creates an element from string
  744. Dropzone.createElement = (string) ->
  745. div = document.createElement "div"
  746. div.innerHTML = string
  747. div.childNodes[0]
  748. # Tests if given element is inside (or simply is) the container
  749. Dropzone.elementInside = (element, container) ->
  750. return yes if element == container # Coffeescript doesn't support do/while loops
  751. return yes while element = element.parentNode when element == container
  752. return no
  753. Dropzone.getElement = (el, name) ->
  754. if typeof el == "string"
  755. element = document.querySelector el
  756. else if el.nodeType?
  757. element = el
  758. throw new Error "Invalid `#{name}` option provided. Please provide a CSS selector or a plain HTML element." unless element?
  759. return element
  760. Dropzone.getElements = (els, name) ->
  761. if els instanceof Array
  762. elements = [ ]
  763. try
  764. elements.push @getElement el, name for el in els
  765. catch e
  766. elements = null
  767. else if typeof els == "string"
  768. elements = [ ]
  769. elements.push el for el in document.querySelectorAll els
  770. else if els.nodeType?
  771. elements = [ els ]
  772. throw new Error "Invalid `#{name}` option provided. Please provide a CSS selector, a plain HTML element or a list of those." unless elements? and elements.length
  773. return elements
  774. # Validates the mime type like this:
  775. #
  776. # https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept
  777. Dropzone.isValidMimeType = (mimeType, acceptedMimeTypes) ->
  778. return yes unless acceptedMimeTypes # If there are no accepted mime types, it's OK
  779. acceptedMimeTypes = acceptedMimeTypes.split ","
  780. baseMimeType = mimeType.replace /\/.*$/, ""
  781. for validMimeType in acceptedMimeTypes
  782. validMimeType = validMimeType.trim()
  783. if /\/\*$/.test validMimeType
  784. # This is something like a image/* mime type
  785. return yes if baseMimeType == validMimeType.replace /\/.*$/, ""
  786. else
  787. return yes if mimeType == validMimeType
  788. return no
  789. # Augment jQuery
  790. if jQuery?
  791. jQuery.fn.dropzone = (options) ->
  792. this.each -> new Dropzone this, options
  793. if module?
  794. module.exports = Dropzone
  795. else
  796. window.Dropzone = Dropzone
  797. # Dropzone file status codes
  798. Dropzone.ADDED = "added"
  799. # Accepted does not necessarely mean queued.
  800. Dropzone.ACCEPTED = "accepted"
  801. Dropzone.QUEUED = "queued"
  802. Dropzone.UPLOADING = "uploading"
  803. Dropzone.PROCESSING = Dropzone.UPLOADING # alias
  804. Dropzone.CANCELED = "canceled"
  805. Dropzone.ERROR = "error"
  806. Dropzone.SUCCESS = "success"
  807. ###
  808. # contentloaded.js
  809. #
  810. # Author: Diego Perini (diego.perini at gmail.com)
  811. # Summary: cross-browser wrapper for DOMContentLoaded
  812. # Updated: 20101020
  813. # License: MIT
  814. # Version: 1.2
  815. #
  816. # URL:
  817. # http://javascript.nwbox.com/ContentLoaded/
  818. # http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE
  819. ###
  820. # @win window reference
  821. # @fn function reference
  822. contentLoaded = (win, fn) ->
  823. done = false
  824. top = true
  825. doc = win.document
  826. root = doc.documentElement
  827. add = (if doc.addEventListener then "addEventListener" else "attachEvent")
  828. rem = (if doc.addEventListener then "removeEventListener" else "detachEvent")
  829. pre = (if doc.addEventListener then "" else "on")
  830. init = (e) ->
  831. return if e.type is "readystatechange" and doc.readyState isnt "complete"
  832. ((if e.type is "load" then win else doc))[rem] pre + e.type, init, false
  833. fn.call win, e.type or e if not done and (done = true)
  834. poll = ->
  835. try
  836. root.doScroll "left"
  837. catch e
  838. setTimeout poll, 50
  839. return
  840. init "poll"
  841. unless doc.readyState is "complete"
  842. if doc.createEventObject and root.doScroll
  843. try
  844. top = not win.frameElement
  845. poll() if top
  846. doc[add] pre + "DOMContentLoaded", init, false
  847. doc[add] pre + "readystatechange", init, false
  848. win[add] pre + "load", init, false
  849. contentLoaded window, Dropzone.discover