3 # More info at [www.dropzonejs.com](http://www.dropzonejs.com)
5 # Copyright (c) 2012, Matias Meno
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:
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
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
31 # The Emitter class provides the ability to call `.on()` on Dropzone to listen
33 # It is strongly based on component's emitter class, and I removed the
34 # functionality because of the dependency hell with different frameworks.
37 # Add an event listener for given event
38 addEventListener: @::on
40 @_callbacks = @_callbacks || {}
41 # Create namespace for this event
42 @_callbacks[event] = [] unless @_callbacks[event]
43 @_callbacks[event].push fn
47 emit: (event, args...) ->
48 @_callbacks = @_callbacks || {}
49 callbacks = @_callbacks[event]
52 callback.apply @, args for callback in callbacks
56 # Remove event listener for given event. If fn is not provided, all event
57 # listeners for that event will be removed. If neither is provided, all
58 # event listeners will be removed.
59 removeListener: @::off
60 removeAllListeners: @::off
61 removeEventListener: @::off
63 if !@_callbacks || arguments.length == 0
68 callbacks = @_callbacks[event]
69 return @ unless callbacks
72 if arguments.length == 1
73 delete @_callbacks[event]
76 # remove specific handler
77 for callback, i in callbacks
84 class Dropzone extends Emitter
86 # Exposing the emitter class, mainly for tests
90 This is a list of all available events you can register on a dropzone object.
92 You can register an event handler like this:
94 dropzone.on("dragEnter", function() { });
113 "totaluploadprogress"
135 uploadMultiple: no # Whether to send multiple files in one request.
136 maxFilesize: 256 # in MB
137 paramName: "file" # The name of the file param that gets transferred.
138 createImageThumbnails: true
139 maxThumbnailFilesize: 10 # in MB. When the filename exceeds this limit, the thumbnail will not be generated.
143 # The base that is used to calculate the filesize. You can change this to
144 # 1024 if you would rather display kibibytes, mebibytes, etc...
145 # 1024 is technically incorrect,
146 # because `1024 bytes` are `1 kibibyte` not `1 kilobyte`.
147 # You can change this to `1024` if you don't care about validity.
150 # Can be used to limit the maximum number of files that will be handled
154 # Can be an object of additional parameters to transfer to the server.
155 # This is the same as adding hidden input fields in the form element.
158 # If true, the dropzone will present a file selector when clicked.
161 # Whether hidden files in directories should be ignored.
162 ignoreHiddenFiles: yes
164 # You can set accepted mime types here.
166 # The default implementation of the `accept()` function will check this
167 # property, and if the Dropzone is clickable this will be used as
168 # `accept` attribute.
170 # This is a comma separated list of mime types or extensions. E.g.:
172 # audio/*,video/*,image/png,.pdf
174 # See https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept
179 # Use acceptedFiles instead.
180 acceptedMimeTypes: null
182 # If false, files will be added to the queue but the queue will not be
183 # processed automatically.
184 # This can be useful if you need some additional user input before sending
185 # files (or if you want want all files sent at once).
186 # If you're ready to send the file simply call myDropzone.processQueue()
189 # If false, files added to the dropzone will not be queued by default.
190 # You'll have to call `enqueueFile(file)` manually.
193 # If true, Dropzone will add a link to each file preview to cancel/remove
195 # See dictCancelUpload and dictRemoveFile to use different words.
198 # A CSS selector or HTML element for the file previews container.
199 # If null, the dropzone element itself will be used.
200 # If false, previews won't be rendered.
201 previewsContainer: null
203 # Selector for hidden input container
204 hiddenInputContainer: "body"
206 # If null, no capture type will be specified
207 # If camera, mobile devices will skip the file selection and choose camera
208 # If microphone, mobile devices will skip the file selection and choose the microphone
209 # If camcorder, mobile devices will skip the file selection and choose the camera in video mode
210 # On apple devices multiple must be set to false. AcceptedFiles may need to
211 # be set to an appropriate mime type (e.g. "image/*", "audio/*", or "video/*").
214 # Before the file is appended to the formData, the function _renameFilename is performed for file.name
215 # which executes the function defined in renameFilename
220 # The text used before any files are dropped
221 dictDefaultMessage: "Drop files here to upload"
223 # The text that replaces the default message text it the browser is not supported
224 dictFallbackMessage: "Your browser does not support drag'n'drop file uploads."
226 # The text that will be added before the fallback form
227 # If null, no text will be added at all.
228 dictFallbackText: "Please use the fallback form below to upload your files like in the olden days."
230 # If the filesize is too big.
231 dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB."
233 # If the file doesn't match the file type.
234 dictInvalidFileType: "You can't upload files of this type."
236 # If the server response was invalid.
237 dictResponseError: "Server responded with {{statusCode}} code."
239 # If used, the text to be used for the cancel upload link.
240 dictCancelUpload: "Cancel upload"
242 # If used, the text to be used for confirmation when cancelling upload.
243 dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?"
245 # If used, the text to be used to remove a file.
246 dictRemoveFile: "Remove file"
248 # If this is not null, then the user will be prompted before removing a file.
249 dictRemoveFileConfirmation: null
251 # Displayed when the maxFiles have been exceeded
252 # You can use {{maxFiles}} here, which will be replaced by the option.
253 dictMaxFilesExceeded: "You can not upload any more files."
256 # If `done()` is called without argument the file is accepted
257 # If you call it with an error message, the file is rejected
258 # (This allows for asynchronous validation).
259 accept: (file, done) -> done()
262 # Called when dropzone initialized
263 # You can add event listeners here
266 # Used to debug dropzone and force the fallback form.
269 # Called when the browser does not support drag and drop
271 # This code should pass in IE7... :(
272 @element.className = "#{@element.className} dz-browser-not-supported"
274 for child in @element.getElementsByTagName "div"
275 if /(^| )dz-message($| )/.test child.className
276 messageElement = child
277 child.className = "dz-message" # Removes the 'dz-default' class
279 unless messageElement
280 messageElement = Dropzone.createElement """<div class="dz-message"><span></span></div>"""
281 @element.appendChild messageElement
283 span = messageElement.getElementsByTagName("span")[0]
286 span.textContent = @options.dictFallbackMessage
287 else if span.innerText?
288 span.innerText = @options.dictFallbackMessage
290 @element.appendChild @getFallbackForm()
294 # Gets called to calculate the thumbnail dimensions.
296 # You can use file.width, file.height, options.thumbnailWidth and
297 # options.thumbnailHeight to calculate the dimensions.
299 # The dimensions are going to be used like this:
301 # var info = @options.resize.call(this, file);
302 # ctx.drawImage(img, info.srcX, info.srcY, info.srcWidth, info.srcHeight, info.trgX, info.trgY, info.trgWidth, info.trgHeight);
304 # srcX, srcy, trgX and trgY can be omitted (in which case 0 is assumed).
305 # trgWidth and trgHeight can be omitted (in which case the options.thumbnailWidth / options.thumbnailHeight are used)
311 srcHeight: file.height
313 srcRatio = file.width / file.height
315 info.optWidth = @options.thumbnailWidth
316 info.optHeight = @options.thumbnailHeight
318 # automatically calculate dimensions if not specified
319 if !info.optWidth? and !info.optHeight?
320 info.optWidth = info.srcWidth
321 info.optHeight = info.srcHeight
322 else if !info.optWidth?
323 info.optWidth = srcRatio * info.optHeight
324 else if !info.optHeight?
325 info.optHeight = (1/srcRatio) * info.optWidth
327 trgRatio = info.optWidth / info.optHeight
329 if file.height < info.optHeight or file.width < info.optWidth
330 # This image is smaller than the canvas
331 info.trgHeight = info.srcHeight
332 info.trgWidth = info.srcWidth
335 # Image is bigger and needs rescaling
336 if srcRatio > trgRatio
337 info.srcHeight = file.height
338 info.srcWidth = info.srcHeight * trgRatio
340 info.srcWidth = file.width
341 info.srcHeight = info.srcWidth / trgRatio
343 info.srcX = (file.width - info.srcWidth) / 2
344 info.srcY = (file.height - info.srcHeight) / 2
350 Those functions register themselves to the events on init and handle all
351 the user interface specific stuff. Overwriting them won't break the upload
352 but can break the way it's displayed.
353 You can overwrite them if you don't like the default behavior. If you just
354 want to add an additional event handler, register it on the dropzone object
355 and don't overwrite those options.
361 # Those are self explanatory and simply concern the DragnDrop.
362 drop: (e) -> @element.classList.remove "dz-drag-hover"
364 dragend: (e) -> @element.classList.remove "dz-drag-hover"
365 dragenter: (e) -> @element.classList.add "dz-drag-hover"
366 dragover: (e) -> @element.classList.add "dz-drag-hover"
367 dragleave: (e) -> @element.classList.remove "dz-drag-hover"
371 # Called whenever there are no files left in the dropzone anymore, and the
372 # dropzone should be displayed as if in the initial state.
374 @element.classList.remove "dz-started"
376 # Called when a file is added to the queue
379 @element.classList.add "dz-started" if @element == @previewsContainer
381 if @previewsContainer
382 file.previewElement = Dropzone.createElement @options.previewTemplate.trim()
383 file.previewTemplate = file.previewElement # Backwards compatibility
385 @previewsContainer.appendChild file.previewElement
386 node.textContent = @_renameFilename(file.name) for node in file.previewElement.querySelectorAll("[data-dz-name]")
387 node.innerHTML = @filesize file.size for node in file.previewElement.querySelectorAll("[data-dz-size]")
389 if @options.addRemoveLinks
390 file._removeLink = Dropzone.createElement """<a class="dz-remove" href="javascript:undefined;" data-dz-remove>#{@options.dictRemoveFile}</a>"""
391 file.previewElement.appendChild file._removeLink
393 removeFileEvent = (e) =>
396 if file.status == Dropzone.UPLOADING
397 Dropzone.confirm @options.dictCancelUploadConfirmation, => @removeFile file
399 if @options.dictRemoveFileConfirmation
400 Dropzone.confirm @options.dictRemoveFileConfirmation, => @removeFile file
404 removeLink.addEventListener "click", removeFileEvent for removeLink in file.previewElement.querySelectorAll("[data-dz-remove]")
407 # Called whenever a file is removed.
408 removedfile: (file) ->
409 file.previewElement?.parentNode.removeChild file.previewElement if file.previewElement
410 @_updateMaxFilesReachedClass()
412 # Called when a thumbnail has been generated
413 # Receives `file` and `dataUrl`
414 thumbnail: (file, dataUrl) ->
415 if file.previewElement
416 file.previewElement.classList.remove "dz-file-preview"
417 for thumbnailElement in file.previewElement.querySelectorAll("[data-dz-thumbnail]")
418 thumbnailElement.alt = file.name
419 thumbnailElement.src = dataUrl
421 setTimeout (=> file.previewElement.classList.add "dz-image-preview"), 1
423 # Called whenever an error occurs
424 # Receives `file` and `message`
425 error: (file, message) ->
426 if file.previewElement
427 file.previewElement.classList.add "dz-error"
428 message = message.error if typeof message != "String" and message.error
429 node.textContent = message for node in file.previewElement.querySelectorAll("[data-dz-errormessage]")
433 # Called when a file gets processed. Since there is a cue, not all added
434 # files are processed immediately.
436 processing: (file) ->
437 if file.previewElement
438 file.previewElement.classList.add "dz-processing"
439 file._removeLink.textContent = @options.dictCancelUpload if file._removeLink
441 processingmultiple: noop
443 # Called whenever the upload progress gets updated.
444 # Receives `file`, `progress` (percentage 0-100) and `bytesSent`.
445 # To get the total number of bytes of the file, use `file.size`
446 uploadprogress: (file, progress, bytesSent) ->
447 if file.previewElement
448 for node in file.previewElement.querySelectorAll("[data-dz-uploadprogress]")
449 if node.nodeName is 'PROGRESS'
450 node.value = progress
452 node.style.width = "#{progress}%"
454 # Called whenever the total upload progress gets updated.
455 # Called with totalUploadProgress (0-100), totalBytes and totalBytesSent
456 totaluploadprogress: noop
458 # Called just before the file is sent. Gets the `xhr` object as second
459 # parameter, so you can modify it (for example to add a CSRF token) and a
460 # `formData` object to add additional information.
463 sendingmultiple: noop
465 # When the complete upload is finished and successful
468 file.previewElement.classList.add "dz-success" if file.previewElement
470 successmultiple: noop
472 # When the upload is canceled.
473 canceled: (file) -> @emit "error", file, "Upload canceled."
475 canceledmultiple: noop
477 # When the upload is finished, either with success or an error.
480 file._removeLink.textContent = @options.dictRemoveFile if file._removeLink
481 file.previewElement.classList.add "dz-complete" if file.previewElement
483 completemultiple: noop
485 maxfilesexceeded: noop
487 maxfilesreached: noop
494 # This template will be chosen when a new file is dropped.
496 <div class="dz-preview dz-file-preview">
497 <div class="dz-image"><img data-dz-thumbnail /></div>
498 <div class="dz-details">
499 <div class="dz-size"><span data-dz-size></span></div>
500 <div class="dz-filename"><span data-dz-name></span></div>
502 <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
503 <div class="dz-error-message"><span data-dz-errormessage></span></div>
504 <div class="dz-success-mark">
505 <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
508 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
509 <path d="M23.5,31.8431458 L17.5852419,25.9283877 C16.0248253,24.3679711 13.4910294,24.366835 11.9289322,25.9289322 C10.3700136,27.4878508 10.3665912,30.0234455 11.9283877,31.5852419 L20.4147581,40.0716123 C20.5133999,40.1702541 20.6159315,40.2626649 20.7218615,40.3488435 C22.2835669,41.8725651 24.794234,41.8626202 26.3461564,40.3106978 L43.3106978,23.3461564 C44.8771021,21.7797521 44.8758057,19.2483887 43.3137085,17.6862915 C41.7547899,16.1273729 39.2176035,16.1255422 37.6538436,17.6893022 L23.5,31.8431458 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z" id="Oval-2" stroke-opacity="0.198794158" stroke="#747474" fill-opacity="0.816519475" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
513 <div class="dz-error-mark">
514 <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
517 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
518 <g id="Check-+-Oval-2" sketch:type="MSLayerGroup" stroke="#747474" stroke-opacity="0.198794158" fill="#FFFFFF" fill-opacity="0.816519475">
519 <path d="M32.6568542,29 L38.3106978,23.3461564 C39.8771021,21.7797521 39.8758057,19.2483887 38.3137085,17.6862915 C36.7547899,16.1273729 34.2176035,16.1255422 32.6538436,17.6893022 L27,23.3431458 L21.3461564,17.6893022 C19.7823965,16.1255422 17.2452101,16.1273729 15.6862915,17.6862915 C14.1241943,19.2483887 14.1228979,21.7797521 15.6893022,23.3461564 L21.3431458,29 L15.6893022,34.6538436 C14.1228979,36.2202479 14.1241943,38.7516113 15.6862915,40.3137085 C17.2452101,41.8726271 19.7823965,41.8744578 21.3461564,40.3106978 L27,34.6568542 L32.6538436,40.3106978 C34.2176035,41.8744578 36.7547899,41.8726271 38.3137085,40.3137085 C39.8758057,38.7516113 39.8771021,36.2202479 38.3106978,34.6538436 L32.6568542,29 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z" id="Oval-2" sketch:type="MSShapeGroup"></path>
528 extend = (target, objects...) ->
529 for object in objects
530 target[key] = val for key, val of object
533 constructor: (@element, options) ->
534 # For backwards compatibility since the version was in the prototype previously
535 @version = Dropzone.version
537 @defaultOptions.previewTemplate = @defaultOptions.previewTemplate.replace /\n*/g, ""
539 @clickableElements = [ ]
541 @files = [] # All files
543 @element = document.querySelector @element if typeof @element == "string"
545 # Not checking if instance of HTMLElement or Element since IE9 is extremely weird.
546 throw new Error "Invalid dropzone element." unless @element and @element.nodeType?
548 throw new Error "Dropzone already attached." if @element.dropzone
550 # Now add this dropzone to the instances.
551 Dropzone.instances.push @
553 # Put the dropzone inside the element itself.
554 @element.dropzone = @
556 elementOptions = Dropzone.optionsForElement(@element) ? { }
558 @options = extend { }, @defaultOptions, elementOptions, options ? { }
560 # If the browser failed, just call the fallback and leave
561 return @options.fallback.call this if @options.forceFallback or !Dropzone.isBrowserSupported()
563 # @options.url = @element.getAttribute "action" unless @options.url?
564 @options.url = @element.getAttribute "action" unless @options.url?
566 throw new Error "No URL provided." unless @options.url
568 throw new Error "You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated." if @options.acceptedFiles and @options.acceptedMimeTypes
570 # Backwards compatibility
571 if @options.acceptedMimeTypes
572 @options.acceptedFiles = @options.acceptedMimeTypes
573 delete @options.acceptedMimeTypes
575 @options.method = @options.method.toUpperCase()
577 if (fallback = @getExistingFallback()) and fallback.parentNode
578 # Remove the fallback
579 fallback.parentNode.removeChild fallback
581 # Display previews in the previewsContainer element or the Dropzone element unless explicitly set to false
582 if @options.previewsContainer != false
583 if @options.previewsContainer
584 @previewsContainer = Dropzone.getElement @options.previewsContainer, "previewsContainer"
586 @previewsContainer = @element
588 if @options.clickable
589 if @options.clickable == yes
590 @clickableElements = [ @element ]
592 @clickableElements = Dropzone.getElements @options.clickable, "clickable"
598 # Returns all files that have been accepted
599 getAcceptedFiles: -> file for file in @files when file.accepted
601 # Returns all files that have been rejected
602 # Not sure when that's going to be useful, but added for completeness.
603 getRejectedFiles: -> file for file in @files when not file.accepted
605 getFilesWithStatus: (status) -> file for file in @files when file.status == status
607 # Returns all files that are in the queue
608 getQueuedFiles: -> @getFilesWithStatus Dropzone.QUEUED
610 getUploadingFiles: -> @getFilesWithStatus Dropzone.UPLOADING
612 getAddedFiles: -> @getFilesWithStatus Dropzone.ADDED
614 # Files that are either queued or uploading
615 getActiveFiles: -> file for file in @files when file.status == Dropzone.UPLOADING or file.status == Dropzone.QUEUED
619 # In case it isn't set already
620 @element.setAttribute("enctype", "multipart/form-data") if @element.tagName == "form"
622 if @element.classList.contains("dropzone") and !@element.querySelector(".dz-message")
623 @element.appendChild Dropzone.createElement """<div class="dz-default dz-message"><span>#{@options.dictDefaultMessage}</span></div>"""
625 if @clickableElements.length
626 setupHiddenFileInput = =>
627 @hiddenFileInput.parentNode.removeChild @hiddenFileInput if @hiddenFileInput
628 @hiddenFileInput = document.createElement "input"
629 @hiddenFileInput.setAttribute "type", "file"
630 @hiddenFileInput.setAttribute "multiple", "multiple" if !@options.maxFiles? || @options.maxFiles > 1
631 @hiddenFileInput.className = "dz-hidden-input"
633 @hiddenFileInput.setAttribute "accept", @options.acceptedFiles if @options.acceptedFiles?
634 @hiddenFileInput.setAttribute "capture", @options.capture if @options.capture?
636 # Not setting `display="none"` because some browsers don't accept clicks
637 # on elements that aren't displayed.
638 @hiddenFileInput.style.visibility = "hidden"
639 @hiddenFileInput.style.position = "absolute"
640 @hiddenFileInput.style.top = "0"
641 @hiddenFileInput.style.left = "0"
642 @hiddenFileInput.style.height = "0"
643 @hiddenFileInput.style.width = "0"
644 document.querySelector(@options.hiddenInputContainer).appendChild @hiddenFileInput
645 @hiddenFileInput.addEventListener "change", =>
646 files = @hiddenFileInput.files
647 @addFile file for file in files if files.length
648 @emit "addedfiles", files
649 setupHiddenFileInput()
650 setupHiddenFileInput()
652 @URL = window.URL ? window.webkitURL
655 # Setup all event listeners on the Dropzone object itself.
656 # They're not in @setupEventListeners() because they shouldn't be removed
657 # again when the dropzone gets disabled.
658 @on eventName, @options[eventName] for eventName in @events
660 @on "uploadprogress", => @updateTotalUploadProgress()
662 @on "removedfile", => @updateTotalUploadProgress()
664 @on "canceled", (file) => @emit "complete", file
666 # Emit a `queuecomplete` event if all files finished uploading.
667 @on "complete", (file) =>
668 if @getAddedFiles().length == 0 and @getUploadingFiles().length == 0 and @getQueuedFiles().length == 0
669 # This needs to be deferred so that `queuecomplete` really triggers after `complete`
670 setTimeout (=> @emit "queuecomplete"), 0
673 noPropagation = (e) ->
678 e.returnValue = false
680 # Create the listeners
691 # Makes it possible to drag files from chrome's download bar
692 # http://stackoverflow.com/questions/19526430/drag-and-drop-file-uploads-from-chrome-downloads-bar
693 # Try is required to prevent bug in Internet Explorer 11 (SCRIPT65535 exception)
694 try efct = e.dataTransfer.effectAllowed
695 e.dataTransfer.dropEffect = if 'move' == efct or 'linkMove' == efct then 'move' else 'copy'
707 # This is disabled right now, because the browsers don't implement it properly.
714 @clickableElements.forEach (clickableElement) =>
716 element: clickableElement
719 # Only the actual dropzone or the message element should trigger file selection
720 if (clickableElement != @element) or (evt.target == @element or Dropzone.elementInside evt.target, @element.querySelector ".dz-message")
721 @hiddenFileInput.click() # Forward the click
728 # Not fully tested yet
732 if @hiddenFileInput?.parentNode
733 @hiddenFileInput.parentNode.removeChild @hiddenFileInput
734 @hiddenFileInput = null
735 delete @element.dropzone
736 Dropzone.instances.splice Dropzone.instances.indexOf(this), 1
739 updateTotalUploadProgress: ->
743 activeFiles = @getActiveFiles()
745 if activeFiles.length
746 for file in @getActiveFiles()
747 totalBytesSent += file.upload.bytesSent
748 totalBytes += file.upload.total
749 totalUploadProgress = 100 * totalBytesSent / totalBytes
751 totalUploadProgress = 100
753 @emit "totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent
755 # @options.paramName can be a function taking one parameter rather than a string.
756 # A parameter name for a file is obtained simply by calling this with an index number.
757 _getParamName: (n) ->
758 if typeof @options.paramName is "function"
761 "#{@options.paramName}#{if @options.uploadMultiple then "[#{n}]" else ""}"
763 # If @options.renameFilename is a function,
764 # the function will be used to rename the file.name before appending it to the formData
765 _renameFilename: (name) ->
766 return name unless typeof @options.renameFilename is "function"
767 @options.renameFilename name
769 # Returns a form that can be used as fallback if the browser does not support DragnDrop
771 # If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided.
772 # This code has to pass in IE7 :(
774 return existingFallback if existingFallback = @getExistingFallback()
776 fieldsString = """<div class="dz-fallback">"""
777 fieldsString += """<p>#{@options.dictFallbackText}</p>""" if @options.dictFallbackText
778 fieldsString += """<input type="file" name="#{@_getParamName 0}" #{if @options.uploadMultiple then 'multiple="multiple"' } /><input type="submit" value="Upload!"></div>"""
780 fields = Dropzone.createElement fieldsString
781 if @element.tagName isnt "FORM"
782 form = Dropzone.createElement("""<form action="#{@options.url}" enctype="multipart/form-data" method="#{@options.method}"></form>""")
783 form.appendChild fields
785 # Make sure that the enctype and method attributes are set properly
786 @element.setAttribute "enctype", "multipart/form-data"
787 @element.setAttribute "method", @options.method
791 # Returns the fallback elements if they exist already
793 # This code has to pass in IE7 :(
794 getExistingFallback: ->
795 getFallback = (elements) -> return el for el in elements when /(^| )fallback($| )/.test el.className
797 for tagName in [ "div", "form" ]
798 return fallback if fallback = getFallback @element.getElementsByTagName tagName
801 # Activates all listeners stored in @listeners
802 setupEventListeners: ->
803 for elementListeners in @listeners
804 elementListeners.element.addEventListener event, listener, false for event, listener of elementListeners.events
807 # Deactivates all listeners stored in @listeners
808 removeEventListeners: ->
809 for elementListeners in @listeners
810 elementListeners.element.removeEventListener event, listener, false for event, listener of elementListeners.events
812 # Removes all event listeners and cancels all files in the queue or being processed.
814 @clickableElements.forEach (element) -> element.classList.remove "dz-clickable"
815 @removeEventListeners()
817 @cancelUpload file for file in @files
820 @clickableElements.forEach (element) -> element.classList.add "dz-clickable"
821 @setupEventListeners()
823 # Returns a nicely formatted filesize
829 units = [ 'TB', 'GB', 'MB', 'KB', 'b' ]
832 cutoff = Math.pow(@options.filesizeBase, 4 - i) / 10
835 selectedSize = size / Math.pow(@options.filesizeBase, 4 - i)
839 selectedSize = Math.round(10 * selectedSize) / 10 # Cutting of digits
841 "<strong>#{selectedSize}</strong> #{selectedUnit}"
844 # Adds or removes the `dz-max-files-reached` class from the form.
845 _updateMaxFilesReachedClass: ->
846 if @options.maxFiles? and @getAcceptedFiles().length >= @options.maxFiles
847 @emit 'maxfilesreached', @files if @getAcceptedFiles().length == @options.maxFiles
848 @element.classList.add "dz-max-files-reached"
850 @element.classList.remove "dz-max-files-reached"
855 return unless e.dataTransfer
858 files = e.dataTransfer.files
859 @emit "addedfiles", files
861 # Even if it's a folder, files.length will contain the folders.
863 items = e.dataTransfer.items
864 if items and items.length and (items[0].webkitGetAsEntry?)
865 # The browser supports dropping of folders, so handle items instead of files
866 @_addFilesFromItems items
872 return unless e?.clipboardData?.items?
875 items = e.clipboardData.items
877 @_addFilesFromItems items if items.length
880 handleFiles: (files) ->
881 @addFile file for file in files
883 # When a folder is dropped (or files are pasted), items must be handled
885 _addFilesFromItems: (items) ->
887 if item.webkitGetAsEntry? and entry = item.webkitGetAsEntry()
889 @addFile item.getAsFile()
890 else if entry.isDirectory
891 # Append all files from that directory to files
892 @_addFilesFromDirectory entry, entry.name
893 else if item.getAsFile?
894 if !item.kind? or item.kind == "file"
895 @addFile item.getAsFile()
898 # Goes through the directory, and adds each file it finds recursively
899 _addFilesFromDirectory: (directory, path) ->
900 dirReader = directory.createReader()
902 errorHandler = (error) -> console?.log? error
905 dirReader.readEntries (entries) =>
906 if entries.length > 0
910 return if @options.ignoreHiddenFiles and file.name.substring(0, 1) is '.'
911 file.fullPath = "#{path}/#{file.name}"
913 else if entry.isDirectory
914 @_addFilesFromDirectory entry, "#{path}/#{entry.name}"
916 # Recursively call readEntries() again, since browser only handle
917 # the first 100 entries.
918 # See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries
927 # If `done()` is called without argument the file is accepted
928 # If you call it with an error message, the file is rejected
929 # (This allows for asynchronous validation)
931 # This function checks the filesize, and if the file.type passes the
932 # `acceptedFiles` check.
933 accept: (file, done) ->
934 if file.size > @options.maxFilesize * 1024 * 1024
935 done @options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", @options.maxFilesize)
936 else unless Dropzone.isValidFile file, @options.acceptedFiles
937 done @options.dictInvalidFileType
938 else if @options.maxFiles? and @getAcceptedFiles().length >= @options.maxFiles
939 done @options.dictMaxFilesExceeded.replace "{{maxFiles}}", @options.maxFiles
940 @emit "maxfilesexceeded", file
942 @options.accept.call this, file, done
947 # Setting the total upload size to file.size for the beginning
948 # It's actual different than the size to be transmitted.
953 file.status = Dropzone.ADDED
955 @emit "addedfile", file
957 @_enqueueThumbnail file
959 @accept file, (error) =>
961 file.accepted = false
962 @_errorProcessing [ file ], error # Will set the file.status
965 @enqueueFile file if @options.autoQueue # Will set .accepted = true
966 @_updateMaxFilesReachedClass()
969 # Wrapper for enqueueFile
970 enqueueFiles: (files) -> @enqueueFile file for file in files; null
972 enqueueFile: (file) ->
973 if file.status == Dropzone.ADDED and file.accepted == true
974 file.status = Dropzone.QUEUED
975 if @options.autoProcessQueue
976 setTimeout (=> @processQueue()), 0 # Deferring the call
978 throw new Error "This file can't be queued because it has already been processed or was rejected."
982 _processingThumbnail: no
983 _enqueueThumbnail: (file) ->
984 if @options.createImageThumbnails and file.type.match(/image.*/) and file.size <= @options.maxThumbnailFilesize * 1024 * 1024
985 @_thumbnailQueue.push(file)
986 setTimeout (=> @_processThumbnailQueue()), 0 # Deferring the call
988 _processThumbnailQueue: ->
989 return if @_processingThumbnail or @_thumbnailQueue.length == 0
991 @_processingThumbnail = yes
992 @createThumbnail @_thumbnailQueue.shift(), =>
993 @_processingThumbnail = no
994 @_processThumbnailQueue()
997 # Can be called by the user to remove a file
998 removeFile: (file) ->
999 @cancelUpload file if file.status == Dropzone.UPLOADING
1000 @files = without @files, file
1002 @emit "removedfile", file
1003 @emit "reset" if @files.length == 0
1005 # Removes all files that aren't currently processed from the list
1006 removeAllFiles: (cancelIfNecessary = off) ->
1007 # Create a copy of files since removeFile() changes the @files array.
1008 for file in @files.slice()
1009 @removeFile file if file.status != Dropzone.UPLOADING || cancelIfNecessary
1012 createThumbnail: (file, callback) ->
1014 fileReader = new FileReader
1016 fileReader.onload = =>
1018 # Don't bother creating a thumbnail for SVG images since they're vector
1019 if file.type == "image/svg+xml"
1020 @emit "thumbnail", file, fileReader.result
1021 callback() if callback?
1024 @createThumbnailFromUrl file, fileReader.result, callback
1026 fileReader.readAsDataURL file
1028 createThumbnailFromUrl: (file, imageUrl, callback, crossOrigin) ->
1029 # Not using `new Image` here because of a bug in latest Chrome versions.
1030 # See https://github.com/enyo/dropzone/pull/226
1031 img = document.createElement "img"
1033 img.crossOrigin = crossOrigin if crossOrigin
1036 file.width = img.width
1037 file.height = img.height
1039 resizeInfo = @options.resize.call @, file
1041 resizeInfo.trgWidth ?= resizeInfo.optWidth
1042 resizeInfo.trgHeight ?= resizeInfo.optHeight
1044 canvas = document.createElement "canvas"
1045 ctx = canvas.getContext "2d"
1046 canvas.width = resizeInfo.trgWidth
1047 canvas.height = resizeInfo.trgHeight
1049 # This is a bugfix for iOS' scaling bug.
1050 drawImageIOSFix ctx, img, resizeInfo.srcX ? 0, resizeInfo.srcY ? 0, resizeInfo.srcWidth, resizeInfo.srcHeight, resizeInfo.trgX ? 0, resizeInfo.trgY ? 0, resizeInfo.trgWidth, resizeInfo.trgHeight
1052 thumbnail = canvas.toDataURL "image/png"
1054 @emit "thumbnail", file, thumbnail
1055 callback() if callback?
1057 img.onerror = callback if callback?
1062 # Goes through the queue and processes files if there aren't too many already.
1064 parallelUploads = @options.parallelUploads
1065 processingLength = @getUploadingFiles().length
1066 i = processingLength
1068 # There are already at least as many files uploading than should be
1069 return if processingLength >= parallelUploads
1071 queuedFiles = @getQueuedFiles()
1073 return unless queuedFiles.length > 0
1075 if @options.uploadMultiple
1076 # The files should be uploaded in one request
1077 @processFiles queuedFiles.slice 0, (parallelUploads - processingLength)
1079 while i < parallelUploads
1080 return unless queuedFiles.length # Nothing left to process
1081 @processFile queuedFiles.shift()
1085 # Wrapper for `processFiles`
1086 processFile: (file) -> @processFiles [ file ]
1089 # Loads the file, then calls finishedLoading()
1090 processFiles: (files) ->
1092 file.processing = yes # Backwards compatibility
1093 file.status = Dropzone.UPLOADING
1095 @emit "processing", file
1097 @emit "processingmultiple", files if @options.uploadMultiple
1103 _getFilesWithXhr: (xhr) -> files = (file for file in @files when file.xhr == xhr)
1106 # Cancels the file upload and sets the status to CANCELED
1107 # **if** the file is actually being uploaded.
1108 # If it's still in the queue, the file is being removed from it and the status
1110 cancelUpload: (file) ->
1111 if file.status == Dropzone.UPLOADING
1112 groupedFiles = @_getFilesWithXhr file.xhr
1113 groupedFile.status = Dropzone.CANCELED for groupedFile in groupedFiles
1115 @emit "canceled", groupedFile for groupedFile in groupedFiles
1116 @emit "canceledmultiple", groupedFiles if @options.uploadMultiple
1118 else if file.status in [ Dropzone.ADDED, Dropzone.QUEUED ]
1119 file.status = Dropzone.CANCELED
1120 @emit "canceled", file
1121 @emit "canceledmultiple", [ file ] if @options.uploadMultiple
1123 @processQueue() if @options.autoProcessQueue
1125 resolveOption = (option, args...) ->
1126 if typeof option == 'function'
1127 return option.apply(@, args)
1130 # Wrapper for uploadFiles()
1131 uploadFile: (file) -> @uploadFiles [ file ]
1133 uploadFiles: (files) ->
1134 xhr = new XMLHttpRequest()
1136 # Put the xhr object in the file objects to be able to reference it later.
1137 file.xhr = xhr for file in files
1139 method = resolveOption @options.method, files
1140 url = resolveOption @options.url, files
1141 xhr.open method, url, true
1143 # Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
1144 xhr.withCredentials = !!@options.withCredentials
1151 @_errorProcessing files, response || @options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr
1154 updateProgress = (e) =>
1156 progress = 100 * e.loaded / e.total
1164 # Called when the file finished uploading
1166 allFilesFinished = yes
1171 allFilesFinished = no unless file.upload.progress == 100 and file.upload.bytesSent == file.upload.total
1172 file.upload.progress = progress
1173 file.upload.bytesSent = file.upload.total
1175 # Nothing to do, all files already at 100%
1176 return if allFilesFinished
1179 @emit "uploadprogress", file, progress, file.upload.bytesSent
1182 return if files[0].status == Dropzone.CANCELED
1184 return unless xhr.readyState is 4
1186 response = xhr.responseText
1188 if xhr.getResponseHeader("content-type") and ~xhr.getResponseHeader("content-type").indexOf "application/json"
1190 response = JSON.parse response
1192 response = "Invalid JSON response from server."
1196 unless 200 <= xhr.status < 300
1199 @_finished files, response, e
1202 return if files[0].status == Dropzone.CANCELED
1205 # Some browsers do not have the .upload property
1206 progressObj = xhr.upload ? xhr
1207 progressObj.onprogress = updateProgress
1210 "Accept": "application/json",
1211 "Cache-Control": "no-cache",
1212 "X-Requested-With": "XMLHttpRequest",
1214 extend headers, @options.headers if @options.headers
1216 for headerName, headerValue of headers
1217 xhr.setRequestHeader headerName, headerValue if headerValue
1219 formData = new FormData()
1221 # Adding all @options parameters
1222 formData.append key, value for key, value of @options.params if @options.params
1224 # Let the user add additional data if necessary
1225 @emit "sending", file, xhr, formData for file in files
1226 @emit "sendingmultiple", files, xhr, formData if @options.uploadMultiple
1229 # Take care of other input elements
1230 if @element.tagName == "FORM"
1231 for input in @element.querySelectorAll "input, textarea, select, button"
1232 inputName = input.getAttribute "name"
1233 inputType = input.getAttribute "type"
1235 if input.tagName == "SELECT" and input.hasAttribute "multiple"
1236 # Possibly multiple values
1237 formData.append inputName, option.value for option in input.options when option.selected
1238 else if !inputType or (inputType.toLowerCase() not in [ "checkbox", "radio" ]) or input.checked
1239 formData.append inputName, input.value
1242 # Finally add the file
1243 # Has to be last because some servers (eg: S3) expect the file to be the
1245 formData.append @_getParamName(i), files[i], @_renameFilename(files[i].name) for i in [0..files.length-1]
1247 @submitRequest xhr, formData, files
1249 submitRequest: (xhr, formData, files) ->
1252 # Called internally when processing is finished.
1253 # Individual callbacks have to be called in the appropriate sections.
1254 _finished: (files, responseText, e) ->
1256 file.status = Dropzone.SUCCESS
1257 @emit "success", file, responseText, e
1258 @emit "complete", file
1259 if @options.uploadMultiple
1260 @emit "successmultiple", files, responseText, e
1261 @emit "completemultiple", files
1263 @processQueue() if @options.autoProcessQueue
1265 # Called internally when processing is finished.
1266 # Individual callbacks have to be called in the appropriate sections.
1267 _errorProcessing: (files, message, xhr) ->
1269 file.status = Dropzone.ERROR
1270 @emit "error", file, message, xhr
1271 @emit "complete", file
1272 if @options.uploadMultiple
1273 @emit "errormultiple", files, message, xhr
1274 @emit "completemultiple", files
1276 @processQueue() if @options.autoProcessQueue
1280 Dropzone.version = "4.3.0"
1283 # This is a map of options for your different dropzones. Add configurations
1284 # to this object for your different dropzone elemens.
1288 # Dropzone.options.myDropzoneElementId = { maxFilesize: 1 };
1290 # To disable autoDiscover for a specific element, you can set `false` as an option:
1292 # Dropzone.options.myDisabledElementId = false;
1296 # <form action="/upload" id="my-dropzone-element-id" class="dropzone"></form>
1297 Dropzone.options = { }
1300 # Returns the options for an element or undefined if none available.
1301 Dropzone.optionsForElement = (element) ->
1302 # Get the `Dropzone.options.elementId` for this element if it exists
1303 if element.getAttribute("id") then Dropzone.options[camelize element.getAttribute "id"] else undefined
1306 # Holds a list of all dropzone instances
1307 Dropzone.instances = [ ]
1309 # Returns the dropzone for given element if any
1310 Dropzone.forElement = (element) ->
1311 element = document.querySelector element if typeof element == "string"
1312 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?
1313 return element.dropzone
1316 # Set to false if you don't want Dropzone to automatically find and attach to .dropzone elements.
1317 Dropzone.autoDiscover = on
1319 # Looks for all .dropzone elements and creates a dropzone for them
1320 Dropzone.discover = ->
1321 if document.querySelectorAll
1322 dropzones = document.querySelectorAll ".dropzone"
1326 checkElements = (elements) ->
1328 dropzones.push el if /(^| )dropzone($| )/.test el.className
1329 checkElements document.getElementsByTagName "div"
1330 checkElements document.getElementsByTagName "form"
1332 for dropzone in dropzones
1333 # Create a dropzone unless auto discover has been disabled for specific element
1334 new Dropzone dropzone unless Dropzone.optionsForElement(dropzone) == false
1338 # Since the whole Drag'n'Drop API is pretty new, some browsers implement it,
1339 # but not correctly.
1340 # So I created a blacklist of userAgents. Yes, yes. Browser sniffing, I know.
1341 # But what to do when browsers *theoretically* support an API, but crash
1344 # This is a list of regular expressions tested against navigator.userAgent
1346 # ** It should only be used on browser that *do* support the API, but
1349 Dropzone.blacklistedBrowsers = [
1350 # The mac os version of opera 12 seems to have a problem with the File drag'n'drop API.
1351 /opera.*Macintosh.*version\/12/i
1356 # Checks if the browser is supported
1357 Dropzone.isBrowserSupported = ->
1358 capableBrowser = yes
1360 if window.File and window.FileReader and window.FileList and window.Blob and window.FormData and document.querySelector
1361 unless "classList" of document.createElement "a"
1364 # The browser supports the API, but may be blacklisted.
1365 for regex in Dropzone.blacklistedBrowsers
1366 if regex.test navigator.userAgent
1377 # Returns an array without the rejected item
1378 without = (list, rejectedItem) -> item for item in list when item isnt rejectedItem
1380 # abc-def_ghi -> abcDefGhi
1381 camelize = (str) -> str.replace /[\-_](\w)/g, (match) -> match.charAt(1).toUpperCase()
1383 # Creates an element from string
1384 Dropzone.createElement = (string) ->
1385 div = document.createElement "div"
1386 div.innerHTML = string
1389 # Tests if given element is inside (or simply is) the container
1390 Dropzone.elementInside = (element, container) ->
1391 return yes if element == container # Coffeescript doesn't support do/while loops
1392 return yes while element = element.parentNode when element == container
1397 Dropzone.getElement = (el, name) ->
1398 if typeof el == "string"
1399 element = document.querySelector el
1400 else if el.nodeType?
1402 throw new Error "Invalid `#{name}` option provided. Please provide a CSS selector or a plain HTML element." unless element?
1406 Dropzone.getElements = (els, name) ->
1407 if els instanceof Array
1410 elements.push @getElement el, name for el in els
1413 else if typeof els == "string"
1415 elements.push el for el in document.querySelectorAll els
1416 else if els.nodeType?
1419 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
1423 # Asks the user the question and calls accepted or rejected accordingly
1425 # The default implementation just uses `window.confirm` and then calls the
1426 # appropriate callback.
1427 Dropzone.confirm = (question, accepted, rejected) ->
1428 if window.confirm question
1433 # Validates the mime type like this:
1435 # https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept
1436 Dropzone.isValidFile = (file, acceptedFiles) ->
1437 return yes unless acceptedFiles # If there are no accepted mime types, it's OK
1438 acceptedFiles = acceptedFiles.split ","
1440 mimeType = file.type
1441 baseMimeType = mimeType.replace /\/.*$/, ""
1443 for validType in acceptedFiles
1444 validType = validType.trim()
1445 if validType.charAt(0) == "."
1446 return yes if file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) != -1
1447 else if /\/\*$/.test validType
1448 # This is something like a image/* mime type
1449 return yes if baseMimeType == validType.replace /\/.*$/, ""
1451 return yes if mimeType == validType
1458 jQuery.fn.dropzone = (options) ->
1459 this.each -> new Dropzone this, options
1465 module.exports = Dropzone
1467 window.Dropzone = Dropzone
1473 # Dropzone file status codes
1474 Dropzone.ADDED = "added"
1476 Dropzone.QUEUED = "queued"
1477 # For backwards compatibility. Now, if a file is accepted, it's either queued
1479 Dropzone.ACCEPTED = Dropzone.QUEUED
1481 Dropzone.UPLOADING = "uploading"
1482 Dropzone.PROCESSING = Dropzone.UPLOADING # alias
1484 Dropzone.CANCELED = "canceled"
1485 Dropzone.ERROR = "error"
1486 Dropzone.SUCCESS = "success"
1494 Bugfix for iOS 6 and 7
1495 Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios
1496 based on the work of https://github.com/stomita/ios-imagefile-megapixel
1500 # Detecting vertical squash in loaded image.
1501 # Fixes a bug which squash image vertically while drawing into canvas for some images.
1502 # This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel
1503 detectVerticalSquash = (img) ->
1504 iw = img.naturalWidth
1505 ih = img.naturalHeight
1506 canvas = document.createElement("canvas")
1509 ctx = canvas.getContext("2d")
1510 ctx.drawImage img, 0, 0
1511 data = ctx.getImageData(0, 0, 1, ih).data
1514 # search image edge pixel position in case it is squashed vertically.
1519 alpha = data[(py - 1) * 4 + 3]
1521 if alpha is 0 then ey = py else sy = py
1526 if (ratio is 0) then 1 else ratio
1528 # A replacement for context.drawImage
1529 # (args are for source and destination).
1530 drawImageIOSFix = (ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) ->
1531 vertSquashRatio = detectVerticalSquash img
1532 ctx.drawImage img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio
1544 # Author: Diego Perini (diego.perini at gmail.com)
1545 # Summary: cross-browser wrapper for DOMContentLoaded
1551 # http://javascript.nwbox.com/ContentLoaded/
1552 # http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE
1555 # @win window reference
1556 # @fn function reference
1557 contentLoaded = (win, fn) ->
1561 root = doc.documentElement
1562 add = (if doc.addEventListener then "addEventListener" else "attachEvent")
1563 rem = (if doc.addEventListener then "removeEventListener" else "detachEvent")
1564 pre = (if doc.addEventListener then "" else "on")
1566 return if e.type is "readystatechange" and doc.readyState isnt "complete"
1567 ((if e.type is "load" then win else doc))[rem] pre + e.type, init, false
1568 fn.call win, e.type or e if not done and (done = true)
1572 root.doScroll "left"
1578 unless doc.readyState is "complete"
1579 if doc.createEventObject and root.doScroll
1581 top = not win.frameElement
1583 doc[add] pre + "DOMContentLoaded", init, false
1584 doc[add] pre + "readystatechange", init, false
1585 win[add] pre + "load", init, false
1588 # As a single function to be able to write tests.
1589 Dropzone._autoDiscoverFunction = -> Dropzone.discover() if Dropzone.autoDiscover
1590 contentLoaded window, Dropzone._autoDiscoverFunction