diff --git a/examples/demo/demo-safe.ecf b/examples/demo/demo-safe.ecf index 2d6c3dc..25d0e56 100644 --- a/examples/demo/demo-safe.ecf +++ b/examples/demo/demo-safe.ecf @@ -8,7 +8,7 @@ /CVS$ /EIFGENs$ - @@ -28,6 +28,7 @@ + @@ -37,12 +38,9 @@ - - - + diff --git a/examples/demo/demo.ini b/examples/demo/demo.ini index e09f0dd..2498319 100644 --- a/examples/demo/demo.ini +++ b/examples/demo/demo.ini @@ -1,2 +1,3 @@ port=9090 +#port=12345 #verbose=true diff --git a/examples/demo/install_modules.bat b/examples/demo/install_modules.bat index 62715f8..a21ac49 100644 --- a/examples/demo/install_modules.bat +++ b/examples/demo/install_modules.bat @@ -1,3 +1,4 @@ +@echo off setlocal set ROC_CMD=call %~dp0..\..\tools\roc.bat set ROC_CMS_DIR=%~dp0 @@ -16,3 +17,4 @@ set ROC_CMS_DIR=%~dp0 %ROC_CMD% install --module ..\..\modules\seo --dir %ROC_CMS_DIR% %ROC_CMD% install --module ..\..\modules\session_auth --dir %ROC_CMS_DIR% %ROC_CMD% install --module ..\..\modules\taxonomy --dir %ROC_CMS_DIR% +%ROC_CMD% install --module ..\..\modules\files --dir %ROC_CMS_DIR% diff --git a/examples/demo/site/modules/files/files/css/files.css b/examples/demo/site/modules/files/files/css/files.css new file mode 100644 index 0000000..d522e67 --- /dev/null +++ b/examples/demo/site/modules/files/files/css/files.css @@ -0,0 +1,52 @@ +.uploaded-files table { + width: 100%; + border-collapse: collapse; + border: 1px solid black; +} +.uploaded-files table th { + padding: 3px 0 3px 5px; +} +.uploaded-files table td { + padding: 3px 0 3px 5px; +} +.uploaded-files a.button { + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; +} +.uploaded-files a.button:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; +} + +.upload-files .center { + text-align: center; + padding: 10px; +} +.upload-files a.button { + margin: auto; + width: 100px; + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; +} +.upload-files a.button:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; +} + +/******************* Drop Zone *******************/ +.dropzone { + width: 100%; + border: 2px dashed blue; + border-radius: 3px; + text-align: center; + padding-top: 15px; + padding-bottom: 15px; +} diff --git a/examples/demo/site/modules/files/files/img/file-logo.png b/examples/demo/site/modules/files/files/img/file-logo.png new file mode 100644 index 0000000..dbe20a7 Binary files /dev/null and b/examples/demo/site/modules/files/files/img/file-logo.png differ diff --git a/examples/demo/site/modules/files/files/js/dropzone.css b/examples/demo/site/modules/files/files/js/dropzone.css new file mode 100644 index 0000000..d04515e --- /dev/null +++ b/examples/demo/site/modules/files/files/js/dropzone.css @@ -0,0 +1 @@ +@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,0.3);background:white;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:0.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:white}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-ms-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,0.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,0.8);background-color:rgba(255,255,255,0.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,0.4);padding:0 0.4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05, 1.05);-moz-transform:scale(1.05, 1.05);-ms-transform:scale(1.05, 1.05);-o-transform:scale(1.05, 1.05);transform:scale(1.05, 1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;-o-transition:all 0.2s linear;transition:all 0.2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity 0.4s ease-in;-moz-transition:opacity 0.4s ease-in;-ms-transition:opacity 0.4s ease-in;-o-transition:opacity 0.4s ease-in;transition:opacity 0.4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,0.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom, #666, #444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width 300ms ease-in-out;-moz-transition:width 300ms ease-in-out;-ms-transition:width 300ms ease-in-out;-o-transition:width 300ms ease-in-out;transition:width 300ms ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity 0.3s ease;-moz-transition:opacity 0.3s ease;-ms-transition:opacity 0.3s ease;-o-transition:opacity 0.3s ease;transition:opacity 0.3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom, #be2626, #a92222);padding:0.5em 1.2em;color:white}.dropzone .dz-preview .dz-error-message:after{content:'';position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626} diff --git a/examples/demo/site/modules/files/files/js/dropzone.js b/examples/demo/site/modules/files/files/js/dropzone.js new file mode 100644 index 0000000..6b72719 --- /dev/null +++ b/examples/demo/site/modules/files/files/js/dropzone.js @@ -0,0 +1,1763 @@ + +/* + * + * More info at [www.dropzonejs.com](http://www.dropzonejs.com) + * + * Copyright (c) 2012, Matias Meno + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +var Dropzone, Emitter, camelize, contentLoaded, detectVerticalSquash, drawImageIOSFix, noop, without, + __slice = [].slice, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + +noop = function() {}; + +Emitter = (function() { + function Emitter() {} + + Emitter.prototype.addEventListener = Emitter.prototype.on; + + Emitter.prototype.on = function(event, fn) { + this._callbacks = this._callbacks || {}; + if (!this._callbacks[event]) { + this._callbacks[event] = []; + } + this._callbacks[event].push(fn); + return this; + }; + + Emitter.prototype.emit = function() { + var args, callback, callbacks, event, _i, _len; + event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + this._callbacks = this._callbacks || {}; + callbacks = this._callbacks[event]; + if (callbacks) { + for (_i = 0, _len = callbacks.length; _i < _len; _i++) { + callback = callbacks[_i]; + callback.apply(this, args); + } + } + return this; + }; + + Emitter.prototype.removeListener = Emitter.prototype.off; + + Emitter.prototype.removeAllListeners = Emitter.prototype.off; + + Emitter.prototype.removeEventListener = Emitter.prototype.off; + + Emitter.prototype.off = function(event, fn) { + var callback, callbacks, i, _i, _len; + if (!this._callbacks || arguments.length === 0) { + this._callbacks = {}; + return this; + } + callbacks = this._callbacks[event]; + if (!callbacks) { + return this; + } + if (arguments.length === 1) { + delete this._callbacks[event]; + return this; + } + for (i = _i = 0, _len = callbacks.length; _i < _len; i = ++_i) { + callback = callbacks[i]; + if (callback === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; + }; + + return Emitter; + +})(); + +Dropzone = (function(_super) { + var extend, resolveOption; + + __extends(Dropzone, _super); + + Dropzone.prototype.Emitter = Emitter; + + + /* + This is a list of all available events you can register on a dropzone object. + + You can register an event handler like this: + + dropzone.on("dragEnter", function() { }); + */ + + Dropzone.prototype.events = ["drop", "dragstart", "dragend", "dragenter", "dragover", "dragleave", "addedfile", "addedfiles", "removedfile", "thumbnail", "error", "errormultiple", "processing", "processingmultiple", "uploadprogress", "totaluploadprogress", "sending", "sendingmultiple", "success", "successmultiple", "canceled", "canceledmultiple", "complete", "completemultiple", "reset", "maxfilesexceeded", "maxfilesreached", "queuecomplete"]; + + Dropzone.prototype.defaultOptions = { + url: null, + method: "post", + withCredentials: false, + parallelUploads: 2, + uploadMultiple: false, + maxFilesize: 256, + paramName: "file", + createImageThumbnails: true, + maxThumbnailFilesize: 10, + thumbnailWidth: 120, + thumbnailHeight: 120, + filesizeBase: 1000, + maxFiles: null, + params: {}, + clickable: true, + ignoreHiddenFiles: true, + acceptedFiles: null, + acceptedMimeTypes: null, + autoProcessQueue: true, + autoQueue: true, + addRemoveLinks: false, + previewsContainer: null, + hiddenInputContainer: "body", + capture: null, + renameFilename: null, + dictDefaultMessage: "Drop files here to upload", + dictFallbackMessage: "Your browser does not support drag'n'drop file uploads.", + dictFallbackText: "Please use the fallback form below to upload your files like in the olden days.", + dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", + dictInvalidFileType: "You can't upload files of this type.", + dictResponseError: "Server responded with {{statusCode}} code.", + dictCancelUpload: "Cancel upload", + dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?", + dictRemoveFile: "Remove file", + dictRemoveFileConfirmation: null, + dictMaxFilesExceeded: "You can not upload any more files.", + accept: function(file, done) { + return done(); + }, + init: function() { + return noop; + }, + forceFallback: false, + fallback: function() { + var child, messageElement, span, _i, _len, _ref; + this.element.className = "" + this.element.className + " dz-browser-not-supported"; + _ref = this.element.getElementsByTagName("div"); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + if (/(^| )dz-message($| )/.test(child.className)) { + messageElement = child; + child.className = "dz-message"; + continue; + } + } + if (!messageElement) { + messageElement = Dropzone.createElement("
"); + this.element.appendChild(messageElement); + } + span = messageElement.getElementsByTagName("span")[0]; + if (span) { + if (span.textContent != null) { + span.textContent = this.options.dictFallbackMessage; + } else if (span.innerText != null) { + span.innerText = this.options.dictFallbackMessage; + } + } + return this.element.appendChild(this.getFallbackForm()); + }, + resize: function(file) { + var info, srcRatio, trgRatio; + info = { + srcX: 0, + srcY: 0, + srcWidth: file.width, + srcHeight: file.height + }; + srcRatio = file.width / file.height; + info.optWidth = this.options.thumbnailWidth; + info.optHeight = this.options.thumbnailHeight; + if ((info.optWidth == null) && (info.optHeight == null)) { + info.optWidth = info.srcWidth; + info.optHeight = info.srcHeight; + } else if (info.optWidth == null) { + info.optWidth = srcRatio * info.optHeight; + } else if (info.optHeight == null) { + info.optHeight = (1 / srcRatio) * info.optWidth; + } + trgRatio = info.optWidth / info.optHeight; + if (file.height < info.optHeight || file.width < info.optWidth) { + info.trgHeight = info.srcHeight; + info.trgWidth = info.srcWidth; + } else { + if (srcRatio > trgRatio) { + info.srcHeight = file.height; + info.srcWidth = info.srcHeight * trgRatio; + } else { + info.srcWidth = file.width; + info.srcHeight = info.srcWidth / trgRatio; + } + } + info.srcX = (file.width - info.srcWidth) / 2; + info.srcY = (file.height - info.srcHeight) / 2; + return info; + }, + + /* + Those functions register themselves to the events on init and handle all + the user interface specific stuff. Overwriting them won't break the upload + but can break the way it's displayed. + You can overwrite them if you don't like the default behavior. If you just + want to add an additional event handler, register it on the dropzone object + and don't overwrite those options. + */ + drop: function(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + dragstart: noop, + dragend: function(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + dragenter: function(e) { + return this.element.classList.add("dz-drag-hover"); + }, + dragover: function(e) { + return this.element.classList.add("dz-drag-hover"); + }, + dragleave: function(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + paste: noop, + reset: function() { + return this.element.classList.remove("dz-started"); + }, + addedfile: function(file) { + var node, removeFileEvent, removeLink, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _results; + if (this.element === this.previewsContainer) { + this.element.classList.add("dz-started"); + } + if (this.previewsContainer) { + file.previewElement = Dropzone.createElement(this.options.previewTemplate.trim()); + file.previewTemplate = file.previewElement; + this.previewsContainer.appendChild(file.previewElement); + _ref = file.previewElement.querySelectorAll("[data-dz-name]"); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + node.textContent = this._renameFilename(file.name); + } + _ref1 = file.previewElement.querySelectorAll("[data-dz-size]"); + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + node = _ref1[_j]; + node.innerHTML = this.filesize(file.size); + } + if (this.options.addRemoveLinks) { + file._removeLink = Dropzone.createElement("" + this.options.dictRemoveFile + ""); + file.previewElement.appendChild(file._removeLink); + } + removeFileEvent = (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + if (file.status === Dropzone.UPLOADING) { + return Dropzone.confirm(_this.options.dictCancelUploadConfirmation, function() { + return _this.removeFile(file); + }); + } else { + if (_this.options.dictRemoveFileConfirmation) { + return Dropzone.confirm(_this.options.dictRemoveFileConfirmation, function() { + return _this.removeFile(file); + }); + } else { + return _this.removeFile(file); + } + } + }; + })(this); + _ref2 = file.previewElement.querySelectorAll("[data-dz-remove]"); + _results = []; + for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { + removeLink = _ref2[_k]; + _results.push(removeLink.addEventListener("click", removeFileEvent)); + } + return _results; + } + }, + removedfile: function(file) { + var _ref; + if (file.previewElement) { + if ((_ref = file.previewElement) != null) { + _ref.parentNode.removeChild(file.previewElement); + } + } + return this._updateMaxFilesReachedClass(); + }, + thumbnail: function(file, dataUrl) { + var thumbnailElement, _i, _len, _ref; + if (file.previewElement) { + file.previewElement.classList.remove("dz-file-preview"); + _ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]"); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + thumbnailElement = _ref[_i]; + thumbnailElement.alt = file.name; + thumbnailElement.src = dataUrl; + } + return setTimeout(((function(_this) { + return function() { + return file.previewElement.classList.add("dz-image-preview"); + }; + })(this)), 1); + } + }, + error: function(file, message) { + var node, _i, _len, _ref, _results; + if (file.previewElement) { + file.previewElement.classList.add("dz-error"); + if (typeof message !== "String" && message.error) { + message = message.error; + } + _ref = file.previewElement.querySelectorAll("[data-dz-errormessage]"); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.textContent = message); + } + return _results; + } + }, + errormultiple: noop, + processing: function(file) { + if (file.previewElement) { + file.previewElement.classList.add("dz-processing"); + if (file._removeLink) { + return file._removeLink.textContent = this.options.dictCancelUpload; + } + } + }, + processingmultiple: noop, + uploadprogress: function(file, progress, bytesSent) { + var node, _i, _len, _ref, _results; + if (file.previewElement) { + _ref = file.previewElement.querySelectorAll("[data-dz-uploadprogress]"); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + if (node.nodeName === 'PROGRESS') { + _results.push(node.value = progress); + } else { + _results.push(node.style.width = "" + progress + "%"); + } + } + return _results; + } + }, + totaluploadprogress: noop, + sending: noop, + sendingmultiple: noop, + success: function(file) { + if (file.previewElement) { + return file.previewElement.classList.add("dz-success"); + } + }, + successmultiple: noop, + canceled: function(file) { + return this.emit("error", file, "Upload canceled."); + }, + canceledmultiple: noop, + complete: function(file) { + if (file._removeLink) { + file._removeLink.textContent = this.options.dictRemoveFile; + } + if (file.previewElement) { + return file.previewElement.classList.add("dz-complete"); + } + }, + completemultiple: noop, + maxfilesexceeded: noop, + maxfilesreached: noop, + queuecomplete: noop, + addedfiles: noop, + previewTemplate: "
\n
\n
\n
\n
\n
\n
\n
\n
\n \n Check\n \n \n \n \n \n
\n
\n \n Error\n \n \n \n \n \n \n \n
\n
" + }; + + extend = function() { + var key, object, objects, target, val, _i, _len; + target = arguments[0], objects = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + for (_i = 0, _len = objects.length; _i < _len; _i++) { + object = objects[_i]; + for (key in object) { + val = object[key]; + target[key] = val; + } + } + return target; + }; + + function Dropzone(element, options) { + var elementOptions, fallback, _ref; + this.element = element; + this.version = Dropzone.version; + this.defaultOptions.previewTemplate = this.defaultOptions.previewTemplate.replace(/\n*/g, ""); + this.clickableElements = []; + this.listeners = []; + this.files = []; + if (typeof this.element === "string") { + this.element = document.querySelector(this.element); + } + if (!(this.element && (this.element.nodeType != null))) { + throw new Error("Invalid dropzone element."); + } + if (this.element.dropzone) { + throw new Error("Dropzone already attached."); + } + Dropzone.instances.push(this); + this.element.dropzone = this; + elementOptions = (_ref = Dropzone.optionsForElement(this.element)) != null ? _ref : {}; + this.options = extend({}, this.defaultOptions, elementOptions, options != null ? options : {}); + if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { + return this.options.fallback.call(this); + } + if (this.options.url == null) { + this.options.url = this.element.getAttribute("action"); + } + if (!this.options.url) { + throw new Error("No URL provided."); + } + if (this.options.acceptedFiles && this.options.acceptedMimeTypes) { + throw new Error("You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated."); + } + if (this.options.acceptedMimeTypes) { + this.options.acceptedFiles = this.options.acceptedMimeTypes; + delete this.options.acceptedMimeTypes; + } + this.options.method = this.options.method.toUpperCase(); + if ((fallback = this.getExistingFallback()) && fallback.parentNode) { + fallback.parentNode.removeChild(fallback); + } + if (this.options.previewsContainer !== false) { + if (this.options.previewsContainer) { + this.previewsContainer = Dropzone.getElement(this.options.previewsContainer, "previewsContainer"); + } else { + this.previewsContainer = this.element; + } + } + if (this.options.clickable) { + if (this.options.clickable === true) { + this.clickableElements = [this.element]; + } else { + this.clickableElements = Dropzone.getElements(this.options.clickable, "clickable"); + } + } + this.init(); + } + + Dropzone.prototype.getAcceptedFiles = function() { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.accepted) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.getRejectedFiles = function() { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (!file.accepted) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.getFilesWithStatus = function(status) { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.status === status) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.getQueuedFiles = function() { + return this.getFilesWithStatus(Dropzone.QUEUED); + }; + + Dropzone.prototype.getUploadingFiles = function() { + return this.getFilesWithStatus(Dropzone.UPLOADING); + }; + + Dropzone.prototype.getAddedFiles = function() { + return this.getFilesWithStatus(Dropzone.ADDED); + }; + + Dropzone.prototype.getActiveFiles = function() { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.status === Dropzone.UPLOADING || file.status === Dropzone.QUEUED) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.init = function() { + var eventName, noPropagation, setupHiddenFileInput, _i, _len, _ref, _ref1; + if (this.element.tagName === "form") { + this.element.setAttribute("enctype", "multipart/form-data"); + } + if (this.element.classList.contains("dropzone") && !this.element.querySelector(".dz-message")) { + this.element.appendChild(Dropzone.createElement("
" + this.options.dictDefaultMessage + "
")); + } + if (this.clickableElements.length) { + setupHiddenFileInput = (function(_this) { + return function() { + if (_this.hiddenFileInput) { + _this.hiddenFileInput.parentNode.removeChild(_this.hiddenFileInput); + } + _this.hiddenFileInput = document.createElement("input"); + _this.hiddenFileInput.setAttribute("type", "file"); + if ((_this.options.maxFiles == null) || _this.options.maxFiles > 1) { + _this.hiddenFileInput.setAttribute("multiple", "multiple"); + } + _this.hiddenFileInput.className = "dz-hidden-input"; + if (_this.options.acceptedFiles != null) { + _this.hiddenFileInput.setAttribute("accept", _this.options.acceptedFiles); + } + if (_this.options.capture != null) { + _this.hiddenFileInput.setAttribute("capture", _this.options.capture); + } + _this.hiddenFileInput.style.visibility = "hidden"; + _this.hiddenFileInput.style.position = "absolute"; + _this.hiddenFileInput.style.top = "0"; + _this.hiddenFileInput.style.left = "0"; + _this.hiddenFileInput.style.height = "0"; + _this.hiddenFileInput.style.width = "0"; + document.querySelector(_this.options.hiddenInputContainer).appendChild(_this.hiddenFileInput); + return _this.hiddenFileInput.addEventListener("change", function() { + var file, files, _i, _len; + files = _this.hiddenFileInput.files; + if (files.length) { + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + _this.addFile(file); + } + } + _this.emit("addedfiles", files); + return setupHiddenFileInput(); + }); + }; + })(this); + setupHiddenFileInput(); + } + this.URL = (_ref = window.URL) != null ? _ref : window.webkitURL; + _ref1 = this.events; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + eventName = _ref1[_i]; + this.on(eventName, this.options[eventName]); + } + this.on("uploadprogress", (function(_this) { + return function() { + return _this.updateTotalUploadProgress(); + }; + })(this)); + this.on("removedfile", (function(_this) { + return function() { + return _this.updateTotalUploadProgress(); + }; + })(this)); + this.on("canceled", (function(_this) { + return function(file) { + return _this.emit("complete", file); + }; + })(this)); + this.on("complete", (function(_this) { + return function(file) { + if (_this.getAddedFiles().length === 0 && _this.getUploadingFiles().length === 0 && _this.getQueuedFiles().length === 0) { + return setTimeout((function() { + return _this.emit("queuecomplete"); + }), 0); + } + }; + })(this)); + noPropagation = function(e) { + e.stopPropagation(); + if (e.preventDefault) { + return e.preventDefault(); + } else { + return e.returnValue = false; + } + }; + this.listeners = [ + { + element: this.element, + events: { + "dragstart": (function(_this) { + return function(e) { + return _this.emit("dragstart", e); + }; + })(this), + "dragenter": (function(_this) { + return function(e) { + noPropagation(e); + return _this.emit("dragenter", e); + }; + })(this), + "dragover": (function(_this) { + return function(e) { + var efct; + try { + efct = e.dataTransfer.effectAllowed; + } catch (_error) {} + e.dataTransfer.dropEffect = 'move' === efct || 'linkMove' === efct ? 'move' : 'copy'; + noPropagation(e); + return _this.emit("dragover", e); + }; + })(this), + "dragleave": (function(_this) { + return function(e) { + return _this.emit("dragleave", e); + }; + })(this), + "drop": (function(_this) { + return function(e) { + noPropagation(e); + return _this.drop(e); + }; + })(this), + "dragend": (function(_this) { + return function(e) { + return _this.emit("dragend", e); + }; + })(this) + } + } + ]; + this.clickableElements.forEach((function(_this) { + return function(clickableElement) { + return _this.listeners.push({ + element: clickableElement, + events: { + "click": function(evt) { + if ((clickableElement !== _this.element) || (evt.target === _this.element || Dropzone.elementInside(evt.target, _this.element.querySelector(".dz-message")))) { + _this.hiddenFileInput.click(); + } + return true; + } + } + }); + }; + })(this)); + this.enable(); + return this.options.init.call(this); + }; + + Dropzone.prototype.destroy = function() { + var _ref; + this.disable(); + this.removeAllFiles(true); + if ((_ref = this.hiddenFileInput) != null ? _ref.parentNode : void 0) { + this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput); + this.hiddenFileInput = null; + } + delete this.element.dropzone; + return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); + }; + + Dropzone.prototype.updateTotalUploadProgress = function() { + var activeFiles, file, totalBytes, totalBytesSent, totalUploadProgress, _i, _len, _ref; + totalBytesSent = 0; + totalBytes = 0; + activeFiles = this.getActiveFiles(); + if (activeFiles.length) { + _ref = this.getActiveFiles(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + totalBytesSent += file.upload.bytesSent; + totalBytes += file.upload.total; + } + totalUploadProgress = 100 * totalBytesSent / totalBytes; + } else { + totalUploadProgress = 100; + } + return this.emit("totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent); + }; + + Dropzone.prototype._getParamName = function(n) { + if (typeof this.options.paramName === "function") { + return this.options.paramName(n); + } else { + return "" + this.options.paramName + (this.options.uploadMultiple ? "[" + n + "]" : ""); + } + }; + + Dropzone.prototype._renameFilename = function(name) { + if (typeof this.options.renameFilename !== "function") { + return name; + } + return this.options.renameFilename(name); + }; + + Dropzone.prototype.getFallbackForm = function() { + var existingFallback, fields, fieldsString, form; + if (existingFallback = this.getExistingFallback()) { + return existingFallback; + } + fieldsString = "
"; + if (this.options.dictFallbackText) { + fieldsString += "

" + this.options.dictFallbackText + "

"; + } + fieldsString += "
"; + fields = Dropzone.createElement(fieldsString); + if (this.element.tagName !== "FORM") { + form = Dropzone.createElement("
"); + form.appendChild(fields); + } else { + this.element.setAttribute("enctype", "multipart/form-data"); + this.element.setAttribute("method", this.options.method); + } + return form != null ? form : fields; + }; + + Dropzone.prototype.getExistingFallback = function() { + var fallback, getFallback, tagName, _i, _len, _ref; + getFallback = function(elements) { + var el, _i, _len; + for (_i = 0, _len = elements.length; _i < _len; _i++) { + el = elements[_i]; + if (/(^| )fallback($| )/.test(el.className)) { + return el; + } + } + }; + _ref = ["div", "form"]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + tagName = _ref[_i]; + if (fallback = getFallback(this.element.getElementsByTagName(tagName))) { + return fallback; + } + } + }; + + Dropzone.prototype.setupEventListeners = function() { + var elementListeners, event, listener, _i, _len, _ref, _results; + _ref = this.listeners; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + elementListeners = _ref[_i]; + _results.push((function() { + var _ref1, _results1; + _ref1 = elementListeners.events; + _results1 = []; + for (event in _ref1) { + listener = _ref1[event]; + _results1.push(elementListeners.element.addEventListener(event, listener, false)); + } + return _results1; + })()); + } + return _results; + }; + + Dropzone.prototype.removeEventListeners = function() { + var elementListeners, event, listener, _i, _len, _ref, _results; + _ref = this.listeners; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + elementListeners = _ref[_i]; + _results.push((function() { + var _ref1, _results1; + _ref1 = elementListeners.events; + _results1 = []; + for (event in _ref1) { + listener = _ref1[event]; + _results1.push(elementListeners.element.removeEventListener(event, listener, false)); + } + return _results1; + })()); + } + return _results; + }; + + Dropzone.prototype.disable = function() { + var file, _i, _len, _ref, _results; + this.clickableElements.forEach(function(element) { + return element.classList.remove("dz-clickable"); + }); + this.removeEventListeners(); + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + _results.push(this.cancelUpload(file)); + } + return _results; + }; + + Dropzone.prototype.enable = function() { + this.clickableElements.forEach(function(element) { + return element.classList.add("dz-clickable"); + }); + return this.setupEventListeners(); + }; + + Dropzone.prototype.filesize = function(size) { + var cutoff, i, selectedSize, selectedUnit, unit, units, _i, _len; + selectedSize = 0; + selectedUnit = "b"; + if (size > 0) { + units = ['TB', 'GB', 'MB', 'KB', 'b']; + for (i = _i = 0, _len = units.length; _i < _len; i = ++_i) { + unit = units[i]; + cutoff = Math.pow(this.options.filesizeBase, 4 - i) / 10; + if (size >= cutoff) { + selectedSize = size / Math.pow(this.options.filesizeBase, 4 - i); + selectedUnit = unit; + break; + } + } + selectedSize = Math.round(10 * selectedSize) / 10; + } + return "" + selectedSize + " " + selectedUnit; + }; + + Dropzone.prototype._updateMaxFilesReachedClass = function() { + if ((this.options.maxFiles != null) && this.getAcceptedFiles().length >= this.options.maxFiles) { + if (this.getAcceptedFiles().length === this.options.maxFiles) { + this.emit('maxfilesreached', this.files); + } + return this.element.classList.add("dz-max-files-reached"); + } else { + return this.element.classList.remove("dz-max-files-reached"); + } + }; + + Dropzone.prototype.drop = function(e) { + var files, items; + if (!e.dataTransfer) { + return; + } + this.emit("drop", e); + files = e.dataTransfer.files; + this.emit("addedfiles", files); + if (files.length) { + items = e.dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry != null)) { + this._addFilesFromItems(items); + } else { + this.handleFiles(files); + } + } + }; + + Dropzone.prototype.paste = function(e) { + var items, _ref; + if ((e != null ? (_ref = e.clipboardData) != null ? _ref.items : void 0 : void 0) == null) { + return; + } + this.emit("paste", e); + items = e.clipboardData.items; + if (items.length) { + return this._addFilesFromItems(items); + } + }; + + Dropzone.prototype.handleFiles = function(files) { + var file, _i, _len, _results; + _results = []; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + _results.push(this.addFile(file)); + } + return _results; + }; + + Dropzone.prototype._addFilesFromItems = function(items) { + var entry, item, _i, _len, _results; + _results = []; + for (_i = 0, _len = items.length; _i < _len; _i++) { + item = items[_i]; + if ((item.webkitGetAsEntry != null) && (entry = item.webkitGetAsEntry())) { + if (entry.isFile) { + _results.push(this.addFile(item.getAsFile())); + } else if (entry.isDirectory) { + _results.push(this._addFilesFromDirectory(entry, entry.name)); + } else { + _results.push(void 0); + } + } else if (item.getAsFile != null) { + if ((item.kind == null) || item.kind === "file") { + _results.push(this.addFile(item.getAsFile())); + } else { + _results.push(void 0); + } + } else { + _results.push(void 0); + } + } + return _results; + }; + + Dropzone.prototype._addFilesFromDirectory = function(directory, path) { + var dirReader, errorHandler, readEntries; + dirReader = directory.createReader(); + errorHandler = function(error) { + return typeof console !== "undefined" && console !== null ? typeof console.log === "function" ? console.log(error) : void 0 : void 0; + }; + readEntries = (function(_this) { + return function() { + return dirReader.readEntries(function(entries) { + var entry, _i, _len; + if (entries.length > 0) { + for (_i = 0, _len = entries.length; _i < _len; _i++) { + entry = entries[_i]; + if (entry.isFile) { + entry.file(function(file) { + if (_this.options.ignoreHiddenFiles && file.name.substring(0, 1) === '.') { + return; + } + file.fullPath = "" + path + "/" + file.name; + return _this.addFile(file); + }); + } else if (entry.isDirectory) { + _this._addFilesFromDirectory(entry, "" + path + "/" + entry.name); + } + } + readEntries(); + } + return null; + }, errorHandler); + }; + })(this); + return readEntries(); + }; + + Dropzone.prototype.accept = function(file, done) { + if (file.size > this.options.maxFilesize * 1024 * 1024) { + return done(this.options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", this.options.maxFilesize)); + } else if (!Dropzone.isValidFile(file, this.options.acceptedFiles)) { + return done(this.options.dictInvalidFileType); + } else if ((this.options.maxFiles != null) && this.getAcceptedFiles().length >= this.options.maxFiles) { + done(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}", this.options.maxFiles)); + return this.emit("maxfilesexceeded", file); + } else { + return this.options.accept.call(this, file, done); + } + }; + + Dropzone.prototype.addFile = function(file) { + file.upload = { + progress: 0, + total: file.size, + bytesSent: 0 + }; + this.files.push(file); + file.status = Dropzone.ADDED; + this.emit("addedfile", file); + this._enqueueThumbnail(file); + return this.accept(file, (function(_this) { + return function(error) { + if (error) { + file.accepted = false; + _this._errorProcessing([file], error); + } else { + file.accepted = true; + if (_this.options.autoQueue) { + _this.enqueueFile(file); + } + } + return _this._updateMaxFilesReachedClass(); + }; + })(this)); + }; + + Dropzone.prototype.enqueueFiles = function(files) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + this.enqueueFile(file); + } + return null; + }; + + Dropzone.prototype.enqueueFile = function(file) { + if (file.status === Dropzone.ADDED && file.accepted === true) { + file.status = Dropzone.QUEUED; + if (this.options.autoProcessQueue) { + return setTimeout(((function(_this) { + return function() { + return _this.processQueue(); + }; + })(this)), 0); + } + } else { + throw new Error("This file can't be queued because it has already been processed or was rejected."); + } + }; + + Dropzone.prototype._thumbnailQueue = []; + + Dropzone.prototype._processingThumbnail = false; + + Dropzone.prototype._enqueueThumbnail = function(file) { + if (this.options.createImageThumbnails && file.type.match(/image.*/) && file.size <= this.options.maxThumbnailFilesize * 1024 * 1024) { + this._thumbnailQueue.push(file); + return setTimeout(((function(_this) { + return function() { + return _this._processThumbnailQueue(); + }; + })(this)), 0); + } + }; + + Dropzone.prototype._processThumbnailQueue = function() { + if (this._processingThumbnail || this._thumbnailQueue.length === 0) { + return; + } + this._processingThumbnail = true; + return this.createThumbnail(this._thumbnailQueue.shift(), (function(_this) { + return function() { + _this._processingThumbnail = false; + return _this._processThumbnailQueue(); + }; + })(this)); + }; + + Dropzone.prototype.removeFile = function(file) { + if (file.status === Dropzone.UPLOADING) { + this.cancelUpload(file); + } + this.files = without(this.files, file); + this.emit("removedfile", file); + if (this.files.length === 0) { + return this.emit("reset"); + } + }; + + Dropzone.prototype.removeAllFiles = function(cancelIfNecessary) { + var file, _i, _len, _ref; + if (cancelIfNecessary == null) { + cancelIfNecessary = false; + } + _ref = this.files.slice(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.status !== Dropzone.UPLOADING || cancelIfNecessary) { + this.removeFile(file); + } + } + return null; + }; + + Dropzone.prototype.createThumbnail = function(file, callback) { + var fileReader; + fileReader = new FileReader; + fileReader.onload = (function(_this) { + return function() { + if (file.type === "image/svg+xml") { + _this.emit("thumbnail", file, fileReader.result); + if (callback != null) { + callback(); + } + return; + } + return _this.createThumbnailFromUrl(file, fileReader.result, callback); + }; + })(this); + return fileReader.readAsDataURL(file); + }; + + Dropzone.prototype.createThumbnailFromUrl = function(file, imageUrl, callback, crossOrigin) { + var img; + img = document.createElement("img"); + if (crossOrigin) { + img.crossOrigin = crossOrigin; + } + img.onload = (function(_this) { + return function() { + var canvas, ctx, resizeInfo, thumbnail, _ref, _ref1, _ref2, _ref3; + file.width = img.width; + file.height = img.height; + resizeInfo = _this.options.resize.call(_this, file); + if (resizeInfo.trgWidth == null) { + resizeInfo.trgWidth = resizeInfo.optWidth; + } + if (resizeInfo.trgHeight == null) { + resizeInfo.trgHeight = resizeInfo.optHeight; + } + canvas = document.createElement("canvas"); + ctx = canvas.getContext("2d"); + canvas.width = resizeInfo.trgWidth; + canvas.height = resizeInfo.trgHeight; + drawImageIOSFix(ctx, img, (_ref = resizeInfo.srcX) != null ? _ref : 0, (_ref1 = resizeInfo.srcY) != null ? _ref1 : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, (_ref2 = resizeInfo.trgX) != null ? _ref2 : 0, (_ref3 = resizeInfo.trgY) != null ? _ref3 : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); + thumbnail = canvas.toDataURL("image/png"); + _this.emit("thumbnail", file, thumbnail); + if (callback != null) { + return callback(); + } + }; + })(this); + if (callback != null) { + img.onerror = callback; + } + return img.src = imageUrl; + }; + + Dropzone.prototype.processQueue = function() { + var i, parallelUploads, processingLength, queuedFiles; + parallelUploads = this.options.parallelUploads; + processingLength = this.getUploadingFiles().length; + i = processingLength; + if (processingLength >= parallelUploads) { + return; + } + queuedFiles = this.getQueuedFiles(); + if (!(queuedFiles.length > 0)) { + return; + } + if (this.options.uploadMultiple) { + return this.processFiles(queuedFiles.slice(0, parallelUploads - processingLength)); + } else { + while (i < parallelUploads) { + if (!queuedFiles.length) { + return; + } + this.processFile(queuedFiles.shift()); + i++; + } + } + }; + + Dropzone.prototype.processFile = function(file) { + return this.processFiles([file]); + }; + + Dropzone.prototype.processFiles = function(files) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.processing = true; + file.status = Dropzone.UPLOADING; + this.emit("processing", file); + } + if (this.options.uploadMultiple) { + this.emit("processingmultiple", files); + } + return this.uploadFiles(files); + }; + + Dropzone.prototype._getFilesWithXhr = function(xhr) { + var file, files; + return files = (function() { + var _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.xhr === xhr) { + _results.push(file); + } + } + return _results; + }).call(this); + }; + + Dropzone.prototype.cancelUpload = function(file) { + var groupedFile, groupedFiles, _i, _j, _len, _len1, _ref; + if (file.status === Dropzone.UPLOADING) { + groupedFiles = this._getFilesWithXhr(file.xhr); + for (_i = 0, _len = groupedFiles.length; _i < _len; _i++) { + groupedFile = groupedFiles[_i]; + groupedFile.status = Dropzone.CANCELED; + } + file.xhr.abort(); + for (_j = 0, _len1 = groupedFiles.length; _j < _len1; _j++) { + groupedFile = groupedFiles[_j]; + this.emit("canceled", groupedFile); + } + if (this.options.uploadMultiple) { + this.emit("canceledmultiple", groupedFiles); + } + } else if ((_ref = file.status) === Dropzone.ADDED || _ref === Dropzone.QUEUED) { + file.status = Dropzone.CANCELED; + this.emit("canceled", file); + if (this.options.uploadMultiple) { + this.emit("canceledmultiple", [file]); + } + } + if (this.options.autoProcessQueue) { + return this.processQueue(); + } + }; + + resolveOption = function() { + var args, option; + option = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + if (typeof option === 'function') { + return option.apply(this, args); + } + return option; + }; + + Dropzone.prototype.uploadFile = function(file) { + return this.uploadFiles([file]); + }; + + Dropzone.prototype.uploadFiles = function(files) { + var file, formData, handleError, headerName, headerValue, headers, i, input, inputName, inputType, key, method, option, progressObj, response, updateProgress, url, value, xhr, _i, _j, _k, _l, _len, _len1, _len2, _len3, _m, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + xhr = new XMLHttpRequest(); + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.xhr = xhr; + } + method = resolveOption(this.options.method, files); + url = resolveOption(this.options.url, files); + xhr.open(method, url, true); + xhr.withCredentials = !!this.options.withCredentials; + response = null; + handleError = (function(_this) { + return function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = files.length; _j < _len1; _j++) { + file = files[_j]; + _results.push(_this._errorProcessing(files, response || _this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr)); + } + return _results; + }; + })(this); + updateProgress = (function(_this) { + return function(e) { + var allFilesFinished, progress, _j, _k, _l, _len1, _len2, _len3, _results; + if (e != null) { + progress = 100 * e.loaded / e.total; + for (_j = 0, _len1 = files.length; _j < _len1; _j++) { + file = files[_j]; + file.upload = { + progress: progress, + total: e.total, + bytesSent: e.loaded + }; + } + } else { + allFilesFinished = true; + progress = 100; + for (_k = 0, _len2 = files.length; _k < _len2; _k++) { + file = files[_k]; + if (!(file.upload.progress === 100 && file.upload.bytesSent === file.upload.total)) { + allFilesFinished = false; + } + file.upload.progress = progress; + file.upload.bytesSent = file.upload.total; + } + if (allFilesFinished) { + return; + } + } + _results = []; + for (_l = 0, _len3 = files.length; _l < _len3; _l++) { + file = files[_l]; + _results.push(_this.emit("uploadprogress", file, progress, file.upload.bytesSent)); + } + return _results; + }; + })(this); + xhr.onload = (function(_this) { + return function(e) { + var _ref; + if (files[0].status === Dropzone.CANCELED) { + return; + } + if (xhr.readyState !== 4) { + return; + } + response = xhr.responseText; + if (xhr.getResponseHeader("content-type") && ~xhr.getResponseHeader("content-type").indexOf("application/json")) { + try { + response = JSON.parse(response); + } catch (_error) { + e = _error; + response = "Invalid JSON response from server."; + } + } + updateProgress(); + if (!((200 <= (_ref = xhr.status) && _ref < 300))) { + return handleError(); + } else { + return _this._finished(files, response, e); + } + }; + })(this); + xhr.onerror = (function(_this) { + return function() { + if (files[0].status === Dropzone.CANCELED) { + return; + } + return handleError(); + }; + })(this); + progressObj = (_ref = xhr.upload) != null ? _ref : xhr; + progressObj.onprogress = updateProgress; + headers = { + "Accept": "application/json", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest" + }; + if (this.options.headers) { + extend(headers, this.options.headers); + } + for (headerName in headers) { + headerValue = headers[headerName]; + if (headerValue) { + xhr.setRequestHeader(headerName, headerValue); + } + } + formData = new FormData(); + if (this.options.params) { + _ref1 = this.options.params; + for (key in _ref1) { + value = _ref1[key]; + formData.append(key, value); + } + } + for (_j = 0, _len1 = files.length; _j < _len1; _j++) { + file = files[_j]; + this.emit("sending", file, xhr, formData); + } + if (this.options.uploadMultiple) { + this.emit("sendingmultiple", files, xhr, formData); + } + if (this.element.tagName === "FORM") { + _ref2 = this.element.querySelectorAll("input, textarea, select, button"); + for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { + input = _ref2[_k]; + inputName = input.getAttribute("name"); + inputType = input.getAttribute("type"); + if (input.tagName === "SELECT" && input.hasAttribute("multiple")) { + _ref3 = input.options; + for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { + option = _ref3[_l]; + if (option.selected) { + formData.append(inputName, option.value); + } + } + } else if (!inputType || ((_ref4 = inputType.toLowerCase()) !== "checkbox" && _ref4 !== "radio") || input.checked) { + formData.append(inputName, input.value); + } + } + } + for (i = _m = 0, _ref5 = files.length - 1; 0 <= _ref5 ? _m <= _ref5 : _m >= _ref5; i = 0 <= _ref5 ? ++_m : --_m) { + formData.append(this._getParamName(i), files[i], this._renameFilename(files[i].name)); + } + return this.submitRequest(xhr, formData, files); + }; + + Dropzone.prototype.submitRequest = function(xhr, formData, files) { + return xhr.send(formData); + }; + + Dropzone.prototype._finished = function(files, responseText, e) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.status = Dropzone.SUCCESS; + this.emit("success", file, responseText, e); + this.emit("complete", file); + } + if (this.options.uploadMultiple) { + this.emit("successmultiple", files, responseText, e); + this.emit("completemultiple", files); + } + if (this.options.autoProcessQueue) { + return this.processQueue(); + } + }; + + Dropzone.prototype._errorProcessing = function(files, message, xhr) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.status = Dropzone.ERROR; + this.emit("error", file, message, xhr); + this.emit("complete", file); + } + if (this.options.uploadMultiple) { + this.emit("errormultiple", files, message, xhr); + this.emit("completemultiple", files); + } + if (this.options.autoProcessQueue) { + return this.processQueue(); + } + }; + + return Dropzone; + +})(Emitter); + +Dropzone.version = "4.3.0"; + +Dropzone.options = {}; + +Dropzone.optionsForElement = function(element) { + if (element.getAttribute("id")) { + return Dropzone.options[camelize(element.getAttribute("id"))]; + } else { + return void 0; + } +}; + +Dropzone.instances = []; + +Dropzone.forElement = function(element) { + if (typeof element === "string") { + element = document.querySelector(element); + } + if ((element != null ? element.dropzone : void 0) == null) { + 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."); + } + return element.dropzone; +}; + +Dropzone.autoDiscover = true; + +Dropzone.discover = function() { + var checkElements, dropzone, dropzones, _i, _len, _results; + if (document.querySelectorAll) { + dropzones = document.querySelectorAll(".dropzone"); + } else { + dropzones = []; + checkElements = function(elements) { + var el, _i, _len, _results; + _results = []; + for (_i = 0, _len = elements.length; _i < _len; _i++) { + el = elements[_i]; + if (/(^| )dropzone($| )/.test(el.className)) { + _results.push(dropzones.push(el)); + } else { + _results.push(void 0); + } + } + return _results; + }; + checkElements(document.getElementsByTagName("div")); + checkElements(document.getElementsByTagName("form")); + } + _results = []; + for (_i = 0, _len = dropzones.length; _i < _len; _i++) { + dropzone = dropzones[_i]; + if (Dropzone.optionsForElement(dropzone) !== false) { + _results.push(new Dropzone(dropzone)); + } else { + _results.push(void 0); + } + } + return _results; +}; + +Dropzone.blacklistedBrowsers = [/opera.*Macintosh.*version\/12/i]; + +Dropzone.isBrowserSupported = function() { + var capableBrowser, regex, _i, _len, _ref; + capableBrowser = true; + if (window.File && window.FileReader && window.FileList && window.Blob && window.FormData && document.querySelector) { + if (!("classList" in document.createElement("a"))) { + capableBrowser = false; + } else { + _ref = Dropzone.blacklistedBrowsers; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + regex = _ref[_i]; + if (regex.test(navigator.userAgent)) { + capableBrowser = false; + continue; + } + } + } + } else { + capableBrowser = false; + } + return capableBrowser; +}; + +without = function(list, rejectedItem) { + var item, _i, _len, _results; + _results = []; + for (_i = 0, _len = list.length; _i < _len; _i++) { + item = list[_i]; + if (item !== rejectedItem) { + _results.push(item); + } + } + return _results; +}; + +camelize = function(str) { + return str.replace(/[\-_](\w)/g, function(match) { + return match.charAt(1).toUpperCase(); + }); +}; + +Dropzone.createElement = function(string) { + var div; + div = document.createElement("div"); + div.innerHTML = string; + return div.childNodes[0]; +}; + +Dropzone.elementInside = function(element, container) { + if (element === container) { + return true; + } + while (element = element.parentNode) { + if (element === container) { + return true; + } + } + return false; +}; + +Dropzone.getElement = function(el, name) { + var element; + if (typeof el === "string") { + element = document.querySelector(el); + } else if (el.nodeType != null) { + element = el; + } + if (element == null) { + throw new Error("Invalid `" + name + "` option provided. Please provide a CSS selector or a plain HTML element."); + } + return element; +}; + +Dropzone.getElements = function(els, name) { + var e, el, elements, _i, _j, _len, _len1, _ref; + if (els instanceof Array) { + elements = []; + try { + for (_i = 0, _len = els.length; _i < _len; _i++) { + el = els[_i]; + elements.push(this.getElement(el, name)); + } + } catch (_error) { + e = _error; + elements = null; + } + } else if (typeof els === "string") { + elements = []; + _ref = document.querySelectorAll(els); + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + el = _ref[_j]; + elements.push(el); + } + } else if (els.nodeType != null) { + elements = [els]; + } + if (!((elements != null) && elements.length)) { + throw new Error("Invalid `" + name + "` option provided. Please provide a CSS selector, a plain HTML element or a list of those."); + } + return elements; +}; + +Dropzone.confirm = function(question, accepted, rejected) { + if (window.confirm(question)) { + return accepted(); + } else if (rejected != null) { + return rejected(); + } +}; + +Dropzone.isValidFile = function(file, acceptedFiles) { + var baseMimeType, mimeType, validType, _i, _len; + if (!acceptedFiles) { + return true; + } + acceptedFiles = acceptedFiles.split(","); + mimeType = file.type; + baseMimeType = mimeType.replace(/\/.*$/, ""); + for (_i = 0, _len = acceptedFiles.length; _i < _len; _i++) { + validType = acceptedFiles[_i]; + validType = validType.trim(); + if (validType.charAt(0) === ".") { + if (file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) !== -1) { + return true; + } + } else if (/\/\*$/.test(validType)) { + if (baseMimeType === validType.replace(/\/.*$/, "")) { + return true; + } + } else { + if (mimeType === validType) { + return true; + } + } + } + return false; +}; + +if (typeof jQuery !== "undefined" && jQuery !== null) { + jQuery.fn.dropzone = function(options) { + return this.each(function() { + return new Dropzone(this, options); + }); + }; +} + +if (typeof module !== "undefined" && module !== null) { + module.exports = Dropzone; +} else { + window.Dropzone = Dropzone; +} + +Dropzone.ADDED = "added"; + +Dropzone.QUEUED = "queued"; + +Dropzone.ACCEPTED = Dropzone.QUEUED; + +Dropzone.UPLOADING = "uploading"; + +Dropzone.PROCESSING = Dropzone.UPLOADING; + +Dropzone.CANCELED = "canceled"; + +Dropzone.ERROR = "error"; + +Dropzone.SUCCESS = "success"; + + +/* + +Bugfix for iOS 6 and 7 +Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios +based on the work of https://github.com/stomita/ios-imagefile-megapixel + */ + +detectVerticalSquash = function(img) { + var alpha, canvas, ctx, data, ey, ih, iw, py, ratio, sy; + iw = img.naturalWidth; + ih = img.naturalHeight; + canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = ih; + ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + data = ctx.getImageData(0, 0, 1, ih).data; + sy = 0; + ey = ih; + py = ih; + while (py > sy) { + alpha = data[(py - 1) * 4 + 3]; + if (alpha === 0) { + ey = py; + } else { + sy = py; + } + py = (ey + sy) >> 1; + } + ratio = py / ih; + if (ratio === 0) { + return 1; + } else { + return ratio; + } +}; + +drawImageIOSFix = function(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) { + var vertSquashRatio; + vertSquashRatio = detectVerticalSquash(img); + return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); +}; + + +/* + * contentloaded.js + * + * Author: Diego Perini (diego.perini at gmail.com) + * Summary: cross-browser wrapper for DOMContentLoaded + * Updated: 20101020 + * License: MIT + * Version: 1.2 + * + * URL: + * http://javascript.nwbox.com/ContentLoaded/ + * http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE + */ + +contentLoaded = function(win, fn) { + var add, doc, done, init, poll, pre, rem, root, top; + done = false; + top = true; + doc = win.document; + root = doc.documentElement; + add = (doc.addEventListener ? "addEventListener" : "attachEvent"); + rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); + pre = (doc.addEventListener ? "" : "on"); + init = function(e) { + if (e.type === "readystatechange" && doc.readyState !== "complete") { + return; + } + (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); + if (!done && (done = true)) { + return fn.call(win, e.type || e); + } + }; + poll = function() { + var e; + try { + root.doScroll("left"); + } catch (_error) { + e = _error; + setTimeout(poll, 50); + return; + } + return init("poll"); + }; + if (doc.readyState !== "complete") { + if (doc.createEventObject && root.doScroll) { + try { + top = !win.frameElement; + } catch (_error) {} + if (top) { + poll(); + } + } + doc[add](pre + "DOMContentLoaded", init, false); + doc[add](pre + "readystatechange", init, false); + return win[add](pre + "load", init, false); + } +}; + +Dropzone._autoDiscoverFunction = function() { + if (Dropzone.autoDiscover) { + return Dropzone.discover(); + } +}; + +contentLoaded(window, Dropzone._autoDiscoverFunction); diff --git a/examples/demo/site/modules/files/files/js/src/basic.scss b/examples/demo/site/modules/files/files/js/src/basic.scss new file mode 100644 index 0000000..52b9329 --- /dev/null +++ b/examples/demo/site/modules/files/files/js/src/basic.scss @@ -0,0 +1,78 @@ +/* + * The MIT License + * Copyright (c) 2012 Matias Meno + */ + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +.dropzone, .dropzone * { + box-sizing: border-box; +} +.dropzone { + + position: relative; + + .dz-preview { + position: relative; + display: inline-block; + width: 120px; + margin: 0.5em; + + .dz-progress { + display: block; + height: 15px; + border: 1px solid #aaa; + .dz-upload { + display: block; + height: 100%; + width: 0; + background: green; + } + } + + .dz-error-message { + color: red; + display: none; + } + &.dz-error { + .dz-error-message, .dz-error-mark { + display: block; + } + } + &.dz-success { + .dz-success-mark { + display: block; + } + } + + .dz-error-mark, .dz-success-mark { + position: absolute; + display: none; + left: 30px; + top: 30px; + width: 54px; + height: 58px; + left: 50%; + margin-left: -(54px/2); + } + + + } + +} \ No newline at end of file diff --git a/examples/demo/site/modules/files/files/js/src/dropzone.coffee b/examples/demo/site/modules/files/files/js/src/dropzone.coffee new file mode 100644 index 0000000..d9bd223 --- /dev/null +++ b/examples/demo/site/modules/files/files/js/src/dropzone.coffee @@ -0,0 +1,1590 @@ +### +# +# More info at [www.dropzonejs.com](http://www.dropzonejs.com) +# +# Copyright (c) 2012, Matias Meno +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +### + + +noop = -> + + +# The Emitter class provides the ability to call `.on()` on Dropzone to listen +# to events. +# It is strongly based on component's emitter class, and I removed the +# functionality because of the dependency hell with different frameworks. +class Emitter + + # Add an event listener for given event + addEventListener: @::on + on: (event, fn) -> + @_callbacks = @_callbacks || {} + # Create namespace for this event + @_callbacks[event] = [] unless @_callbacks[event] + @_callbacks[event].push fn + return @ + + + emit: (event, args...) -> + @_callbacks = @_callbacks || {} + callbacks = @_callbacks[event] + + if callbacks + callback.apply @, args for callback in callbacks + + return @ + + # Remove event listener for given event. If fn is not provided, all event + # listeners for that event will be removed. If neither is provided, all + # event listeners will be removed. + removeListener: @::off + removeAllListeners: @::off + removeEventListener: @::off + off: (event, fn) -> + if !@_callbacks || arguments.length == 0 + @_callbacks = {} + return @ + + # specific event + callbacks = @_callbacks[event] + return @ unless callbacks + + # remove all handlers + if arguments.length == 1 + delete @_callbacks[event] + return @ + + # remove specific handler + for callback, i in callbacks + if callback == fn + callbacks.splice i, 1 + break + + return @ + +class Dropzone extends Emitter + + # Exposing the emitter class, mainly for tests + Emitter: Emitter + + ### + This is a list of all available events you can register on a dropzone object. + + You can register an event handler like this: + + dropzone.on("dragEnter", function() { }); + + ### + events: [ + "drop" + "dragstart" + "dragend" + "dragenter" + "dragover" + "dragleave" + "addedfile" + "addedfiles" + "removedfile" + "thumbnail" + "error" + "errormultiple" + "processing" + "processingmultiple" + "uploadprogress" + "totaluploadprogress" + "sending" + "sendingmultiple" + "success" + "successmultiple" + "canceled" + "canceledmultiple" + "complete" + "completemultiple" + "reset" + "maxfilesexceeded" + "maxfilesreached" + "queuecomplete" + ] + + + + defaultOptions: + url: null + method: "post" + withCredentials: no + parallelUploads: 2 + uploadMultiple: no # Whether to send multiple files in one request. + maxFilesize: 256 # in MB + paramName: "file" # The name of the file param that gets transferred. + createImageThumbnails: true + maxThumbnailFilesize: 10 # in MB. When the filename exceeds this limit, the thumbnail will not be generated. + thumbnailWidth: 120 + thumbnailHeight: 120 + + # The base that is used to calculate the filesize. You can change this to + # 1024 if you would rather display kibibytes, mebibytes, etc... + # 1024 is technically incorrect, + # because `1024 bytes` are `1 kibibyte` not `1 kilobyte`. + # You can change this to `1024` if you don't care about validity. + filesizeBase: 1000 + + # Can be used to limit the maximum number of files that will be handled + # by this Dropzone + maxFiles: null + + # Can be an object of additional parameters to transfer to the server. + # This is the same as adding hidden input fields in the form element. + params: { } + + # If true, the dropzone will present a file selector when clicked. + clickable: yes + + # Whether hidden files in directories should be ignored. + ignoreHiddenFiles: yes + + # You can set accepted mime types here. + # + # The default implementation of the `accept()` function will check this + # property, and if the Dropzone is clickable this will be used as + # `accept` attribute. + # + # This is a comma separated list of mime types or extensions. E.g.: + # + # audio/*,video/*,image/png,.pdf + # + # See https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept + # for a reference. + acceptedFiles: null + + # @deprecated + # Use acceptedFiles instead. + acceptedMimeTypes: null + + # If false, files will be added to the queue but the queue will not be + # processed automatically. + # This can be useful if you need some additional user input before sending + # files (or if you want want all files sent at once). + # If you're ready to send the file simply call myDropzone.processQueue() + autoProcessQueue: on + + # If false, files added to the dropzone will not be queued by default. + # You'll have to call `enqueueFile(file)` manually. + autoQueue: on + + # If true, Dropzone will add a link to each file preview to cancel/remove + # the upload. + # See dictCancelUpload and dictRemoveFile to use different words. + addRemoveLinks: no + + # A CSS selector or HTML element for the file previews container. + # If null, the dropzone element itself will be used. + # If false, previews won't be rendered. + previewsContainer: null + + # Selector for hidden input container + hiddenInputContainer: "body" + + # If null, no capture type will be specified + # If camera, mobile devices will skip the file selection and choose camera + # If microphone, mobile devices will skip the file selection and choose the microphone + # If camcorder, mobile devices will skip the file selection and choose the camera in video mode + # On apple devices multiple must be set to false. AcceptedFiles may need to + # be set to an appropriate mime type (e.g. "image/*", "audio/*", or "video/*"). + capture: null + + # Before the file is appended to the formData, the function _renameFilename is performed for file.name + # which executes the function defined in renameFilename + renameFilename: null + + # Dictionary + + # The text used before any files are dropped + dictDefaultMessage: "Drop files here to upload" + + # The text that replaces the default message text it the browser is not supported + dictFallbackMessage: "Your browser does not support drag'n'drop file uploads." + + # The text that will be added before the fallback form + # If null, no text will be added at all. + dictFallbackText: "Please use the fallback form below to upload your files like in the olden days." + + # If the filesize is too big. + dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB." + + # If the file doesn't match the file type. + dictInvalidFileType: "You can't upload files of this type." + + # If the server response was invalid. + dictResponseError: "Server responded with {{statusCode}} code." + + # If used, the text to be used for the cancel upload link. + dictCancelUpload: "Cancel upload" + + # If used, the text to be used for confirmation when cancelling upload. + dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?" + + # If used, the text to be used to remove a file. + dictRemoveFile: "Remove file" + + # If this is not null, then the user will be prompted before removing a file. + dictRemoveFileConfirmation: null + + # Displayed when the maxFiles have been exceeded + # You can use {{maxFiles}} here, which will be replaced by the option. + dictMaxFilesExceeded: "You can not upload any more files." + + + # If `done()` is called without argument the file is accepted + # If you call it with an error message, the file is rejected + # (This allows for asynchronous validation). + accept: (file, done) -> done() + + + # Called when dropzone initialized + # You can add event listeners here + init: -> noop + + # Used to debug dropzone and force the fallback form. + forceFallback: off + + # Called when the browser does not support drag and drop + fallback: -> + # This code should pass in IE7... :( + @element.className = "#{@element.className} dz-browser-not-supported" + + for child in @element.getElementsByTagName "div" + if /(^| )dz-message($| )/.test child.className + messageElement = child + child.className = "dz-message" # Removes the 'dz-default' class + continue + unless messageElement + messageElement = Dropzone.createElement """
""" + @element.appendChild messageElement + + span = messageElement.getElementsByTagName("span")[0] + if span + if span.textContent? + span.textContent = @options.dictFallbackMessage + else if span.innerText? + span.innerText = @options.dictFallbackMessage + + @element.appendChild @getFallbackForm() + + + + # Gets called to calculate the thumbnail dimensions. + # + # You can use file.width, file.height, options.thumbnailWidth and + # options.thumbnailHeight to calculate the dimensions. + # + # The dimensions are going to be used like this: + # + # var info = @options.resize.call(this, file); + # ctx.drawImage(img, info.srcX, info.srcY, info.srcWidth, info.srcHeight, info.trgX, info.trgY, info.trgWidth, info.trgHeight); + # + # srcX, srcy, trgX and trgY can be omitted (in which case 0 is assumed). + # trgWidth and trgHeight can be omitted (in which case the options.thumbnailWidth / options.thumbnailHeight are used) + resize: (file) -> + info = + srcX: 0 + srcY: 0 + srcWidth: file.width + srcHeight: file.height + + srcRatio = file.width / file.height + + info.optWidth = @options.thumbnailWidth + info.optHeight = @options.thumbnailHeight + + # automatically calculate dimensions if not specified + if !info.optWidth? and !info.optHeight? + info.optWidth = info.srcWidth + info.optHeight = info.srcHeight + else if !info.optWidth? + info.optWidth = srcRatio * info.optHeight + else if !info.optHeight? + info.optHeight = (1/srcRatio) * info.optWidth + + trgRatio = info.optWidth / info.optHeight + + if file.height < info.optHeight or file.width < info.optWidth + # This image is smaller than the canvas + info.trgHeight = info.srcHeight + info.trgWidth = info.srcWidth + + else + # Image is bigger and needs rescaling + if srcRatio > trgRatio + info.srcHeight = file.height + info.srcWidth = info.srcHeight * trgRatio + else + info.srcWidth = file.width + info.srcHeight = info.srcWidth / trgRatio + + info.srcX = (file.width - info.srcWidth) / 2 + info.srcY = (file.height - info.srcHeight) / 2 + + return info + + + ### + Those functions register themselves to the events on init and handle all + the user interface specific stuff. Overwriting them won't break the upload + but can break the way it's displayed. + You can overwrite them if you don't like the default behavior. If you just + want to add an additional event handler, register it on the dropzone object + and don't overwrite those options. + ### + + + + + # Those are self explanatory and simply concern the DragnDrop. + drop: (e) -> @element.classList.remove "dz-drag-hover" + dragstart: noop + dragend: (e) -> @element.classList.remove "dz-drag-hover" + dragenter: (e) -> @element.classList.add "dz-drag-hover" + dragover: (e) -> @element.classList.add "dz-drag-hover" + dragleave: (e) -> @element.classList.remove "dz-drag-hover" + + paste: noop + + # Called whenever there are no files left in the dropzone anymore, and the + # dropzone should be displayed as if in the initial state. + reset: -> + @element.classList.remove "dz-started" + + # Called when a file is added to the queue + # Receives `file` + addedfile: (file) -> + @element.classList.add "dz-started" if @element == @previewsContainer + + if @previewsContainer + file.previewElement = Dropzone.createElement @options.previewTemplate.trim() + file.previewTemplate = file.previewElement # Backwards compatibility + + @previewsContainer.appendChild file.previewElement + node.textContent = @_renameFilename(file.name) for node in file.previewElement.querySelectorAll("[data-dz-name]") + node.innerHTML = @filesize file.size for node in file.previewElement.querySelectorAll("[data-dz-size]") + + if @options.addRemoveLinks + file._removeLink = Dropzone.createElement """#{@options.dictRemoveFile}""" + file.previewElement.appendChild file._removeLink + + removeFileEvent = (e) => + e.preventDefault() + e.stopPropagation() + if file.status == Dropzone.UPLOADING + Dropzone.confirm @options.dictCancelUploadConfirmation, => @removeFile file + else + if @options.dictRemoveFileConfirmation + Dropzone.confirm @options.dictRemoveFileConfirmation, => @removeFile file + else + @removeFile file + + removeLink.addEventListener "click", removeFileEvent for removeLink in file.previewElement.querySelectorAll("[data-dz-remove]") + + + # Called whenever a file is removed. + removedfile: (file) -> + file.previewElement?.parentNode.removeChild file.previewElement if file.previewElement + @_updateMaxFilesReachedClass() + + # Called when a thumbnail has been generated + # Receives `file` and `dataUrl` + thumbnail: (file, dataUrl) -> + if file.previewElement + file.previewElement.classList.remove "dz-file-preview" + for thumbnailElement in file.previewElement.querySelectorAll("[data-dz-thumbnail]") + thumbnailElement.alt = file.name + thumbnailElement.src = dataUrl + + setTimeout (=> file.previewElement.classList.add "dz-image-preview"), 1 + + # Called whenever an error occurs + # Receives `file` and `message` + error: (file, message) -> + if file.previewElement + file.previewElement.classList.add "dz-error" + message = message.error if typeof message != "String" and message.error + node.textContent = message for node in file.previewElement.querySelectorAll("[data-dz-errormessage]") + + errormultiple: noop + + # Called when a file gets processed. Since there is a cue, not all added + # files are processed immediately. + # Receives `file` + processing: (file) -> + if file.previewElement + file.previewElement.classList.add "dz-processing" + file._removeLink.textContent = @options.dictCancelUpload if file._removeLink + + processingmultiple: noop + + # Called whenever the upload progress gets updated. + # Receives `file`, `progress` (percentage 0-100) and `bytesSent`. + # To get the total number of bytes of the file, use `file.size` + uploadprogress: (file, progress, bytesSent) -> + if file.previewElement + for node in file.previewElement.querySelectorAll("[data-dz-uploadprogress]") + if node.nodeName is 'PROGRESS' + node.value = progress + else + node.style.width = "#{progress}%" + + # Called whenever the total upload progress gets updated. + # Called with totalUploadProgress (0-100), totalBytes and totalBytesSent + totaluploadprogress: noop + + # Called just before the file is sent. Gets the `xhr` object as second + # parameter, so you can modify it (for example to add a CSRF token) and a + # `formData` object to add additional information. + sending: noop + + sendingmultiple: noop + + # When the complete upload is finished and successful + # Receives `file` + success: (file) -> + file.previewElement.classList.add "dz-success" if file.previewElement + + successmultiple: noop + + # When the upload is canceled. + canceled: (file) -> @emit "error", file, "Upload canceled." + + canceledmultiple: noop + + # When the upload is finished, either with success or an error. + # Receives `file` + complete: (file) -> + file._removeLink.textContent = @options.dictRemoveFile if file._removeLink + file.previewElement.classList.add "dz-complete" if file.previewElement + + completemultiple: noop + + maxfilesexceeded: noop + + maxfilesreached: noop + + queuecomplete: noop + + addedfiles: noop + + + # This template will be chosen when a new file is dropped. + previewTemplate: """ +
+
+
+
+
+
+
+
+
+ + Check + + + + + +
+
+ + Error + + + + + + + +
+
+ """ + + # global utility + extend = (target, objects...) -> + for object in objects + target[key] = val for key, val of object + target + + constructor: (@element, options) -> + # For backwards compatibility since the version was in the prototype previously + @version = Dropzone.version + + @defaultOptions.previewTemplate = @defaultOptions.previewTemplate.replace /\n*/g, "" + + @clickableElements = [ ] + @listeners = [ ] + @files = [] # All files + + @element = document.querySelector @element if typeof @element == "string" + + # Not checking if instance of HTMLElement or Element since IE9 is extremely weird. + throw new Error "Invalid dropzone element." unless @element and @element.nodeType? + + throw new Error "Dropzone already attached." if @element.dropzone + + # Now add this dropzone to the instances. + Dropzone.instances.push @ + + # Put the dropzone inside the element itself. + @element.dropzone = @ + + elementOptions = Dropzone.optionsForElement(@element) ? { } + + @options = extend { }, @defaultOptions, elementOptions, options ? { } + + # If the browser failed, just call the fallback and leave + return @options.fallback.call this if @options.forceFallback or !Dropzone.isBrowserSupported() + + # @options.url = @element.getAttribute "action" unless @options.url? + @options.url = @element.getAttribute "action" unless @options.url? + + throw new Error "No URL provided." unless @options.url + + throw new Error "You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated." if @options.acceptedFiles and @options.acceptedMimeTypes + + # Backwards compatibility + if @options.acceptedMimeTypes + @options.acceptedFiles = @options.acceptedMimeTypes + delete @options.acceptedMimeTypes + + @options.method = @options.method.toUpperCase() + + if (fallback = @getExistingFallback()) and fallback.parentNode + # Remove the fallback + fallback.parentNode.removeChild fallback + + # Display previews in the previewsContainer element or the Dropzone element unless explicitly set to false + if @options.previewsContainer != false + if @options.previewsContainer + @previewsContainer = Dropzone.getElement @options.previewsContainer, "previewsContainer" + else + @previewsContainer = @element + + if @options.clickable + if @options.clickable == yes + @clickableElements = [ @element ] + else + @clickableElements = Dropzone.getElements @options.clickable, "clickable" + + + @init() + + + # Returns all files that have been accepted + getAcceptedFiles: -> file for file in @files when file.accepted + + # Returns all files that have been rejected + # Not sure when that's going to be useful, but added for completeness. + getRejectedFiles: -> file for file in @files when not file.accepted + + getFilesWithStatus: (status) -> file for file in @files when file.status == status + + # Returns all files that are in the queue + getQueuedFiles: -> @getFilesWithStatus Dropzone.QUEUED + + getUploadingFiles: -> @getFilesWithStatus Dropzone.UPLOADING + + getAddedFiles: -> @getFilesWithStatus Dropzone.ADDED + + # Files that are either queued or uploading + getActiveFiles: -> file for file in @files when file.status == Dropzone.UPLOADING or file.status == Dropzone.QUEUED + + + init: -> + # In case it isn't set already + @element.setAttribute("enctype", "multipart/form-data") if @element.tagName == "form" + + if @element.classList.contains("dropzone") and !@element.querySelector(".dz-message") + @element.appendChild Dropzone.createElement """
#{@options.dictDefaultMessage}
""" + + if @clickableElements.length + setupHiddenFileInput = => + @hiddenFileInput.parentNode.removeChild @hiddenFileInput if @hiddenFileInput + @hiddenFileInput = document.createElement "input" + @hiddenFileInput.setAttribute "type", "file" + @hiddenFileInput.setAttribute "multiple", "multiple" if !@options.maxFiles? || @options.maxFiles > 1 + @hiddenFileInput.className = "dz-hidden-input" + + @hiddenFileInput.setAttribute "accept", @options.acceptedFiles if @options.acceptedFiles? + @hiddenFileInput.setAttribute "capture", @options.capture if @options.capture? + + # Not setting `display="none"` because some browsers don't accept clicks + # on elements that aren't displayed. + @hiddenFileInput.style.visibility = "hidden" + @hiddenFileInput.style.position = "absolute" + @hiddenFileInput.style.top = "0" + @hiddenFileInput.style.left = "0" + @hiddenFileInput.style.height = "0" + @hiddenFileInput.style.width = "0" + document.querySelector(@options.hiddenInputContainer).appendChild @hiddenFileInput + @hiddenFileInput.addEventListener "change", => + files = @hiddenFileInput.files + @addFile file for file in files if files.length + @emit "addedfiles", files + setupHiddenFileInput() + setupHiddenFileInput() + + @URL = window.URL ? window.webkitURL + + + # Setup all event listeners on the Dropzone object itself. + # They're not in @setupEventListeners() because they shouldn't be removed + # again when the dropzone gets disabled. + @on eventName, @options[eventName] for eventName in @events + + @on "uploadprogress", => @updateTotalUploadProgress() + + @on "removedfile", => @updateTotalUploadProgress() + + @on "canceled", (file) => @emit "complete", file + + # Emit a `queuecomplete` event if all files finished uploading. + @on "complete", (file) => + if @getAddedFiles().length == 0 and @getUploadingFiles().length == 0 and @getQueuedFiles().length == 0 + # This needs to be deferred so that `queuecomplete` really triggers after `complete` + setTimeout (=> @emit "queuecomplete"), 0 + + + noPropagation = (e) -> + e.stopPropagation() + if e.preventDefault + e.preventDefault() + else + e.returnValue = false + + # Create the listeners + @listeners = [ + { + element: @element + events: + "dragstart": (e) => + @emit "dragstart", e + "dragenter": (e) => + noPropagation e + @emit "dragenter", e + "dragover": (e) => + # Makes it possible to drag files from chrome's download bar + # http://stackoverflow.com/questions/19526430/drag-and-drop-file-uploads-from-chrome-downloads-bar + # Try is required to prevent bug in Internet Explorer 11 (SCRIPT65535 exception) + try efct = e.dataTransfer.effectAllowed + e.dataTransfer.dropEffect = if 'move' == efct or 'linkMove' == efct then 'move' else 'copy' + + noPropagation e + @emit "dragover", e + "dragleave": (e) => + @emit "dragleave", e + "drop": (e) => + noPropagation e + @drop e + "dragend": (e) => + @emit "dragend", e + + # This is disabled right now, because the browsers don't implement it properly. + # "paste": (e) => + # noPropagation e + # @paste e + } + ] + + @clickableElements.forEach (clickableElement) => + @listeners.push + element: clickableElement + events: + "click": (evt) => + # Only the actual dropzone or the message element should trigger file selection + if (clickableElement != @element) or (evt.target == @element or Dropzone.elementInside evt.target, @element.querySelector ".dz-message") + @hiddenFileInput.click() # Forward the click + return true + + @enable() + + @options.init.call @ + + # Not fully tested yet + destroy: -> + @disable() + @removeAllFiles true + if @hiddenFileInput?.parentNode + @hiddenFileInput.parentNode.removeChild @hiddenFileInput + @hiddenFileInput = null + delete @element.dropzone + Dropzone.instances.splice Dropzone.instances.indexOf(this), 1 + + + updateTotalUploadProgress: -> + totalBytesSent = 0 + totalBytes = 0 + + activeFiles = @getActiveFiles() + + if activeFiles.length + for file in @getActiveFiles() + totalBytesSent += file.upload.bytesSent + totalBytes += file.upload.total + totalUploadProgress = 100 * totalBytesSent / totalBytes + else + totalUploadProgress = 100 + + @emit "totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent + + # @options.paramName can be a function taking one parameter rather than a string. + # A parameter name for a file is obtained simply by calling this with an index number. + _getParamName: (n) -> + if typeof @options.paramName is "function" + @options.paramName n + else + "#{@options.paramName}#{if @options.uploadMultiple then "[#{n}]" else ""}" + + # If @options.renameFilename is a function, + # the function will be used to rename the file.name before appending it to the formData + _renameFilename: (name) -> + return name unless typeof @options.renameFilename is "function" + @options.renameFilename name + + # Returns a form that can be used as fallback if the browser does not support DragnDrop + # + # If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided. + # This code has to pass in IE7 :( + getFallbackForm: -> + return existingFallback if existingFallback = @getExistingFallback() + + fieldsString = """
""" + fieldsString += """

#{@options.dictFallbackText}

""" if @options.dictFallbackText + fieldsString += """
""" + + fields = Dropzone.createElement fieldsString + if @element.tagName isnt "FORM" + form = Dropzone.createElement("""
""") + form.appendChild fields + else + # Make sure that the enctype and method attributes are set properly + @element.setAttribute "enctype", "multipart/form-data" + @element.setAttribute "method", @options.method + form ? fields + + + # Returns the fallback elements if they exist already + # + # This code has to pass in IE7 :( + getExistingFallback: -> + getFallback = (elements) -> return el for el in elements when /(^| )fallback($| )/.test el.className + + for tagName in [ "div", "form" ] + return fallback if fallback = getFallback @element.getElementsByTagName tagName + + + # Activates all listeners stored in @listeners + setupEventListeners: -> + for elementListeners in @listeners + elementListeners.element.addEventListener event, listener, false for event, listener of elementListeners.events + + + # Deactivates all listeners stored in @listeners + removeEventListeners: -> + for elementListeners in @listeners + elementListeners.element.removeEventListener event, listener, false for event, listener of elementListeners.events + + # Removes all event listeners and cancels all files in the queue or being processed. + disable: -> + @clickableElements.forEach (element) -> element.classList.remove "dz-clickable" + @removeEventListeners() + + @cancelUpload file for file in @files + + enable: -> + @clickableElements.forEach (element) -> element.classList.add "dz-clickable" + @setupEventListeners() + + # Returns a nicely formatted filesize + filesize: (size) -> + selectedSize = 0 + selectedUnit = "b" + + if size > 0 + units = [ 'TB', 'GB', 'MB', 'KB', 'b' ] + + for unit, i in units + cutoff = Math.pow(@options.filesizeBase, 4 - i) / 10 + + if size >= cutoff + selectedSize = size / Math.pow(@options.filesizeBase, 4 - i) + selectedUnit = unit + break + + selectedSize = Math.round(10 * selectedSize) / 10 # Cutting of digits + + "#{selectedSize} #{selectedUnit}" + + + # Adds or removes the `dz-max-files-reached` class from the form. + _updateMaxFilesReachedClass: -> + if @options.maxFiles? and @getAcceptedFiles().length >= @options.maxFiles + @emit 'maxfilesreached', @files if @getAcceptedFiles().length == @options.maxFiles + @element.classList.add "dz-max-files-reached" + else + @element.classList.remove "dz-max-files-reached" + + + + drop: (e) -> + return unless e.dataTransfer + @emit "drop", e + + files = e.dataTransfer.files + @emit "addedfiles", files + + # Even if it's a folder, files.length will contain the folders. + if files.length + items = e.dataTransfer.items + if items and items.length and (items[0].webkitGetAsEntry?) + # The browser supports dropping of folders, so handle items instead of files + @_addFilesFromItems items + else + @handleFiles files + return + + paste: (e) -> + return unless e?.clipboardData?.items? + + @emit "paste", e + items = e.clipboardData.items + + @_addFilesFromItems items if items.length + + + handleFiles: (files) -> + @addFile file for file in files + + # When a folder is dropped (or files are pasted), items must be handled + # instead of files. + _addFilesFromItems: (items) -> + for item in items + if item.webkitGetAsEntry? and entry = item.webkitGetAsEntry() + if entry.isFile + @addFile item.getAsFile() + else if entry.isDirectory + # Append all files from that directory to files + @_addFilesFromDirectory entry, entry.name + else if item.getAsFile? + if !item.kind? or item.kind == "file" + @addFile item.getAsFile() + + + # Goes through the directory, and adds each file it finds recursively + _addFilesFromDirectory: (directory, path) -> + dirReader = directory.createReader() + + errorHandler = (error) -> console?.log? error + + readEntries = () => + dirReader.readEntries (entries) => + if entries.length > 0 + for entry in entries + if entry.isFile + entry.file (file) => + return if @options.ignoreHiddenFiles and file.name.substring(0, 1) is '.' + file.fullPath = "#{path}/#{file.name}" + @addFile file + else if entry.isDirectory + @_addFilesFromDirectory entry, "#{path}/#{entry.name}" + + # Recursively call readEntries() again, since browser only handle + # the first 100 entries. + # See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries + readEntries() + return null + , errorHandler + + readEntries() + + + + # If `done()` is called without argument the file is accepted + # If you call it with an error message, the file is rejected + # (This allows for asynchronous validation) + # + # This function checks the filesize, and if the file.type passes the + # `acceptedFiles` check. + accept: (file, done) -> + if file.size > @options.maxFilesize * 1024 * 1024 + done @options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", @options.maxFilesize) + else unless Dropzone.isValidFile file, @options.acceptedFiles + done @options.dictInvalidFileType + else if @options.maxFiles? and @getAcceptedFiles().length >= @options.maxFiles + done @options.dictMaxFilesExceeded.replace "{{maxFiles}}", @options.maxFiles + @emit "maxfilesexceeded", file + else + @options.accept.call this, file, done + + addFile: (file) -> + file.upload = + progress: 0 + # Setting the total upload size to file.size for the beginning + # It's actual different than the size to be transmitted. + total: file.size + bytesSent: 0 + @files.push file + + file.status = Dropzone.ADDED + + @emit "addedfile", file + + @_enqueueThumbnail file + + @accept file, (error) => + if error + file.accepted = false + @_errorProcessing [ file ], error # Will set the file.status + else + file.accepted = true + @enqueueFile file if @options.autoQueue # Will set .accepted = true + @_updateMaxFilesReachedClass() + + + # Wrapper for enqueueFile + enqueueFiles: (files) -> @enqueueFile file for file in files; null + + enqueueFile: (file) -> + if file.status == Dropzone.ADDED and file.accepted == true + file.status = Dropzone.QUEUED + if @options.autoProcessQueue + setTimeout (=> @processQueue()), 0 # Deferring the call + else + throw new Error "This file can't be queued because it has already been processed or was rejected." + + + _thumbnailQueue: [ ] + _processingThumbnail: no + _enqueueThumbnail: (file) -> + if @options.createImageThumbnails and file.type.match(/image.*/) and file.size <= @options.maxThumbnailFilesize * 1024 * 1024 + @_thumbnailQueue.push(file) + setTimeout (=> @_processThumbnailQueue()), 0 # Deferring the call + + _processThumbnailQueue: -> + return if @_processingThumbnail or @_thumbnailQueue.length == 0 + + @_processingThumbnail = yes + @createThumbnail @_thumbnailQueue.shift(), => + @_processingThumbnail = no + @_processThumbnailQueue() + + + # Can be called by the user to remove a file + removeFile: (file) -> + @cancelUpload file if file.status == Dropzone.UPLOADING + @files = without @files, file + + @emit "removedfile", file + @emit "reset" if @files.length == 0 + + # Removes all files that aren't currently processed from the list + removeAllFiles: (cancelIfNecessary = off) -> + # Create a copy of files since removeFile() changes the @files array. + for file in @files.slice() + @removeFile file if file.status != Dropzone.UPLOADING || cancelIfNecessary + return null + + createThumbnail: (file, callback) -> + + fileReader = new FileReader + + fileReader.onload = => + + # Don't bother creating a thumbnail for SVG images since they're vector + if file.type == "image/svg+xml" + @emit "thumbnail", file, fileReader.result + callback() if callback? + return + + @createThumbnailFromUrl file, fileReader.result, callback + + fileReader.readAsDataURL file + + createThumbnailFromUrl: (file, imageUrl, callback, crossOrigin) -> + # Not using `new Image` here because of a bug in latest Chrome versions. + # See https://github.com/enyo/dropzone/pull/226 + img = document.createElement "img" + + img.crossOrigin = crossOrigin if crossOrigin + + img.onload = => + file.width = img.width + file.height = img.height + + resizeInfo = @options.resize.call @, file + + resizeInfo.trgWidth ?= resizeInfo.optWidth + resizeInfo.trgHeight ?= resizeInfo.optHeight + + canvas = document.createElement "canvas" + ctx = canvas.getContext "2d" + canvas.width = resizeInfo.trgWidth + canvas.height = resizeInfo.trgHeight + + # This is a bugfix for iOS' scaling bug. + drawImageIOSFix ctx, img, resizeInfo.srcX ? 0, resizeInfo.srcY ? 0, resizeInfo.srcWidth, resizeInfo.srcHeight, resizeInfo.trgX ? 0, resizeInfo.trgY ? 0, resizeInfo.trgWidth, resizeInfo.trgHeight + + thumbnail = canvas.toDataURL "image/png" + + @emit "thumbnail", file, thumbnail + callback() if callback? + + img.onerror = callback if callback? + + img.src = imageUrl + + + # Goes through the queue and processes files if there aren't too many already. + processQueue: -> + parallelUploads = @options.parallelUploads + processingLength = @getUploadingFiles().length + i = processingLength + + # There are already at least as many files uploading than should be + return if processingLength >= parallelUploads + + queuedFiles = @getQueuedFiles() + + return unless queuedFiles.length > 0 + + if @options.uploadMultiple + # The files should be uploaded in one request + @processFiles queuedFiles.slice 0, (parallelUploads - processingLength) + else + while i < parallelUploads + return unless queuedFiles.length # Nothing left to process + @processFile queuedFiles.shift() + i++ + + + # Wrapper for `processFiles` + processFile: (file) -> @processFiles [ file ] + + + # Loads the file, then calls finishedLoading() + processFiles: (files) -> + for file in files + file.processing = yes # Backwards compatibility + file.status = Dropzone.UPLOADING + + @emit "processing", file + + @emit "processingmultiple", files if @options.uploadMultiple + + @uploadFiles files + + + + _getFilesWithXhr: (xhr) -> files = (file for file in @files when file.xhr == xhr) + + + # Cancels the file upload and sets the status to CANCELED + # **if** the file is actually being uploaded. + # If it's still in the queue, the file is being removed from it and the status + # set to CANCELED. + cancelUpload: (file) -> + if file.status == Dropzone.UPLOADING + groupedFiles = @_getFilesWithXhr file.xhr + groupedFile.status = Dropzone.CANCELED for groupedFile in groupedFiles + file.xhr.abort() + @emit "canceled", groupedFile for groupedFile in groupedFiles + @emit "canceledmultiple", groupedFiles if @options.uploadMultiple + + else if file.status in [ Dropzone.ADDED, Dropzone.QUEUED ] + file.status = Dropzone.CANCELED + @emit "canceled", file + @emit "canceledmultiple", [ file ] if @options.uploadMultiple + + @processQueue() if @options.autoProcessQueue + + resolveOption = (option, args...) -> + if typeof option == 'function' + return option.apply(@, args) + option + + # Wrapper for uploadFiles() + uploadFile: (file) -> @uploadFiles [ file ] + + uploadFiles: (files) -> + xhr = new XMLHttpRequest() + + # Put the xhr object in the file objects to be able to reference it later. + file.xhr = xhr for file in files + + method = resolveOption @options.method, files + url = resolveOption @options.url, files + xhr.open method, url, true + + # Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179 + xhr.withCredentials = !!@options.withCredentials + + + response = null + + handleError = => + for file in files + @_errorProcessing files, response || @options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr + + + updateProgress = (e) => + if e? + progress = 100 * e.loaded / e.total + + for file in files + file.upload = + progress: progress + total: e.total + bytesSent: e.loaded + else + # Called when the file finished uploading + + allFilesFinished = yes + + progress = 100 + + for file in files + allFilesFinished = no unless file.upload.progress == 100 and file.upload.bytesSent == file.upload.total + file.upload.progress = progress + file.upload.bytesSent = file.upload.total + + # Nothing to do, all files already at 100% + return if allFilesFinished + + for file in files + @emit "uploadprogress", file, progress, file.upload.bytesSent + + xhr.onload = (e) => + return if files[0].status == Dropzone.CANCELED + + return unless xhr.readyState is 4 + + response = xhr.responseText + + if xhr.getResponseHeader("content-type") and ~xhr.getResponseHeader("content-type").indexOf "application/json" + try + response = JSON.parse response + catch e + response = "Invalid JSON response from server." + + updateProgress() + + unless 200 <= xhr.status < 300 + handleError() + else + @_finished files, response, e + + xhr.onerror = => + return if files[0].status == Dropzone.CANCELED + handleError() + + # Some browsers do not have the .upload property + progressObj = xhr.upload ? xhr + progressObj.onprogress = updateProgress + + headers = + "Accept": "application/json", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest", + + extend headers, @options.headers if @options.headers + + for headerName, headerValue of headers + xhr.setRequestHeader headerName, headerValue if headerValue + + formData = new FormData() + + # Adding all @options parameters + formData.append key, value for key, value of @options.params if @options.params + + # Let the user add additional data if necessary + @emit "sending", file, xhr, formData for file in files + @emit "sendingmultiple", files, xhr, formData if @options.uploadMultiple + + + # Take care of other input elements + if @element.tagName == "FORM" + for input in @element.querySelectorAll "input, textarea, select, button" + inputName = input.getAttribute "name" + inputType = input.getAttribute "type" + + if input.tagName == "SELECT" and input.hasAttribute "multiple" + # Possibly multiple values + formData.append inputName, option.value for option in input.options when option.selected + else if !inputType or (inputType.toLowerCase() not in [ "checkbox", "radio" ]) or input.checked + formData.append inputName, input.value + + + # Finally add the file + # Has to be last because some servers (eg: S3) expect the file to be the + # last parameter + formData.append @_getParamName(i), files[i], @_renameFilename(files[i].name) for i in [0..files.length-1] + + @submitRequest xhr, formData, files + + submitRequest: (xhr, formData, files) -> + xhr.send formData + + # Called internally when processing is finished. + # Individual callbacks have to be called in the appropriate sections. + _finished: (files, responseText, e) -> + for file in files + file.status = Dropzone.SUCCESS + @emit "success", file, responseText, e + @emit "complete", file + if @options.uploadMultiple + @emit "successmultiple", files, responseText, e + @emit "completemultiple", files + + @processQueue() if @options.autoProcessQueue + + # Called internally when processing is finished. + # Individual callbacks have to be called in the appropriate sections. + _errorProcessing: (files, message, xhr) -> + for file in files + file.status = Dropzone.ERROR + @emit "error", file, message, xhr + @emit "complete", file + if @options.uploadMultiple + @emit "errormultiple", files, message, xhr + @emit "completemultiple", files + + @processQueue() if @options.autoProcessQueue + + + +Dropzone.version = "4.3.0" + + +# This is a map of options for your different dropzones. Add configurations +# to this object for your different dropzone elemens. +# +# Example: +# +# Dropzone.options.myDropzoneElementId = { maxFilesize: 1 }; +# +# To disable autoDiscover for a specific element, you can set `false` as an option: +# +# Dropzone.options.myDisabledElementId = false; +# +# And in html: +# +#
+Dropzone.options = { } + + +# Returns the options for an element or undefined if none available. +Dropzone.optionsForElement = (element) -> + # Get the `Dropzone.options.elementId` for this element if it exists + if element.getAttribute("id") then Dropzone.options[camelize element.getAttribute "id"] else undefined + + +# Holds a list of all dropzone instances +Dropzone.instances = [ ] + +# Returns the dropzone for given element if any +Dropzone.forElement = (element) -> + element = document.querySelector element if typeof element == "string" + 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? + return element.dropzone + + +# Set to false if you don't want Dropzone to automatically find and attach to .dropzone elements. +Dropzone.autoDiscover = on + +# Looks for all .dropzone elements and creates a dropzone for them +Dropzone.discover = -> + if document.querySelectorAll + dropzones = document.querySelectorAll ".dropzone" + else + dropzones = [ ] + # IE :( + checkElements = (elements) -> + for el in elements + dropzones.push el if /(^| )dropzone($| )/.test el.className + checkElements document.getElementsByTagName "div" + checkElements document.getElementsByTagName "form" + + for dropzone in dropzones + # Create a dropzone unless auto discover has been disabled for specific element + new Dropzone dropzone unless Dropzone.optionsForElement(dropzone) == false + + + +# Since the whole Drag'n'Drop API is pretty new, some browsers implement it, +# but not correctly. +# So I created a blacklist of userAgents. Yes, yes. Browser sniffing, I know. +# But what to do when browsers *theoretically* support an API, but crash +# when using it. +# +# This is a list of regular expressions tested against navigator.userAgent +# +# ** It should only be used on browser that *do* support the API, but +# incorrectly ** +# +Dropzone.blacklistedBrowsers = [ + # The mac os version of opera 12 seems to have a problem with the File drag'n'drop API. + /opera.*Macintosh.*version\/12/i + # /MSIE\ 10/i +] + + +# Checks if the browser is supported +Dropzone.isBrowserSupported = -> + capableBrowser = yes + + if window.File and window.FileReader and window.FileList and window.Blob and window.FormData and document.querySelector + unless "classList" of document.createElement "a" + capableBrowser = no + else + # The browser supports the API, but may be blacklisted. + for regex in Dropzone.blacklistedBrowsers + if regex.test navigator.userAgent + capableBrowser = no + continue + else + capableBrowser = no + + capableBrowser + + + + +# Returns an array without the rejected item +without = (list, rejectedItem) -> item for item in list when item isnt rejectedItem + +# abc-def_ghi -> abcDefGhi +camelize = (str) -> str.replace /[\-_](\w)/g, (match) -> match.charAt(1).toUpperCase() + +# Creates an element from string +Dropzone.createElement = (string) -> + div = document.createElement "div" + div.innerHTML = string + div.childNodes[0] + +# Tests if given element is inside (or simply is) the container +Dropzone.elementInside = (element, container) -> + return yes if element == container # Coffeescript doesn't support do/while loops + return yes while element = element.parentNode when element == container + return no + + + +Dropzone.getElement = (el, name) -> + if typeof el == "string" + element = document.querySelector el + else if el.nodeType? + element = el + throw new Error "Invalid `#{name}` option provided. Please provide a CSS selector or a plain HTML element." unless element? + return element + + +Dropzone.getElements = (els, name) -> + if els instanceof Array + elements = [ ] + try + elements.push @getElement el, name for el in els + catch e + elements = null + else if typeof els == "string" + elements = [ ] + elements.push el for el in document.querySelectorAll els + else if els.nodeType? + elements = [ els ] + + 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 + + return elements + +# Asks the user the question and calls accepted or rejected accordingly +# +# The default implementation just uses `window.confirm` and then calls the +# appropriate callback. +Dropzone.confirm = (question, accepted, rejected) -> + if window.confirm question + accepted() + else if rejected? + rejected() + +# Validates the mime type like this: +# +# https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept +Dropzone.isValidFile = (file, acceptedFiles) -> + return yes unless acceptedFiles # If there are no accepted mime types, it's OK + acceptedFiles = acceptedFiles.split "," + + mimeType = file.type + baseMimeType = mimeType.replace /\/.*$/, "" + + for validType in acceptedFiles + validType = validType.trim() + if validType.charAt(0) == "." + return yes if file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) != -1 + else if /\/\*$/.test validType + # This is something like a image/* mime type + return yes if baseMimeType == validType.replace /\/.*$/, "" + else + return yes if mimeType == validType + + return no + + +# Augment jQuery +if jQuery? + jQuery.fn.dropzone = (options) -> + this.each -> new Dropzone this, options + + + + +if module? + module.exports = Dropzone +else + window.Dropzone = Dropzone + + + + + +# Dropzone file status codes +Dropzone.ADDED = "added" + +Dropzone.QUEUED = "queued" +# For backwards compatibility. Now, if a file is accepted, it's either queued +# or uploading. +Dropzone.ACCEPTED = Dropzone.QUEUED + +Dropzone.UPLOADING = "uploading" +Dropzone.PROCESSING = Dropzone.UPLOADING # alias + +Dropzone.CANCELED = "canceled" +Dropzone.ERROR = "error" +Dropzone.SUCCESS = "success" + + + + + +### + +Bugfix for iOS 6 and 7 +Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios +based on the work of https://github.com/stomita/ios-imagefile-megapixel + +### + +# Detecting vertical squash in loaded image. +# Fixes a bug which squash image vertically while drawing into canvas for some images. +# This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel +detectVerticalSquash = (img) -> + iw = img.naturalWidth + ih = img.naturalHeight + canvas = document.createElement("canvas") + canvas.width = 1 + canvas.height = ih + ctx = canvas.getContext("2d") + ctx.drawImage img, 0, 0 + data = ctx.getImageData(0, 0, 1, ih).data + + + # search image edge pixel position in case it is squashed vertically. + sy = 0 + ey = ih + py = ih + while py > sy + alpha = data[(py - 1) * 4 + 3] + + if alpha is 0 then ey = py else sy = py + + py = (ey + sy) >> 1 + ratio = (py / ih) + + if (ratio is 0) then 1 else ratio + +# A replacement for context.drawImage +# (args are for source and destination). +drawImageIOSFix = (ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) -> + vertSquashRatio = detectVerticalSquash img + ctx.drawImage img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio + + + + + + + + +### +# contentloaded.js +# +# Author: Diego Perini (diego.perini at gmail.com) +# Summary: cross-browser wrapper for DOMContentLoaded +# Updated: 20101020 +# License: MIT +# Version: 1.2 +# +# URL: +# http://javascript.nwbox.com/ContentLoaded/ +# http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE +### + +# @win window reference +# @fn function reference +contentLoaded = (win, fn) -> + done = false + top = true + doc = win.document + root = doc.documentElement + add = (if doc.addEventListener then "addEventListener" else "attachEvent") + rem = (if doc.addEventListener then "removeEventListener" else "detachEvent") + pre = (if doc.addEventListener then "" else "on") + init = (e) -> + return if e.type is "readystatechange" and doc.readyState isnt "complete" + ((if e.type is "load" then win else doc))[rem] pre + e.type, init, false + fn.call win, e.type or e if not done and (done = true) + + poll = -> + try + root.doScroll "left" + catch e + setTimeout poll, 50 + return + init "poll" + + unless doc.readyState is "complete" + if doc.createEventObject and root.doScroll + try + top = not win.frameElement + poll() if top + doc[add] pre + "DOMContentLoaded", init, false + doc[add] pre + "readystatechange", init, false + win[add] pre + "load", init, false + + +# As a single function to be able to write tests. +Dropzone._autoDiscoverFunction = -> Dropzone.discover() if Dropzone.autoDiscover +contentLoaded window, Dropzone._autoDiscoverFunction diff --git a/examples/demo/site/modules/files/files/js/src/dropzone.scss b/examples/demo/site/modules/files/files/js/src/dropzone.scss new file mode 100644 index 0000000..b99bede --- /dev/null +++ b/examples/demo/site/modules/files/files/js/src/dropzone.scss @@ -0,0 +1,413 @@ +/* + * The MIT License + * Copyright (c) 2012 Matias Meno + */ + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +@mixin keyframes($name) { + @-webkit-keyframes #{$name} { + @content; + } + @-moz-keyframes #{$name} { + @content; + } + @keyframes #{$name} { + @content; + } +} + + +@mixin prefix($map, $vendors: webkit moz ms o) { + @each $prop, $value in $map { + @if $vendors { + @each $vendor in $vendors { + #{"-" + $vendor + "-" + $prop}: #{$value}; + } + } + // Dump regular property anyway + #{$prop}: #{$value}; + } +} + + +@include keyframes(passing-through) { + + 0% { + opacity: 0; + @include prefix((transform: translateY(40px))); + } + + 30%, 70% { + opacity: 1; + @include prefix((transform: translateY(0px))); + } + + 100% { + opacity: 0; + @include prefix((transform: translateY(-40px))); + } +} + + +@include keyframes(slide-in) { + + 0% { + opacity: 0; + @include prefix((transform: translateY(40px))); + } + + 30% { + opacity: 1; + @include prefix((transform: translateY(0px))); + } +} + + + +@include keyframes(pulse) { + + 0% { @include prefix((transform: scale(1))); } + 10% { @include prefix((transform: scale(1.1))); } + 20% { @include prefix((transform: scale(1))); } + +} + + + +.dropzone, .dropzone * { + box-sizing: border-box; +} +.dropzone { + + $image-size: 120px; + + $image-border-radius: 20px; + + &.dz-clickable { + cursor: pointer; + + * { + cursor: default; + } + .dz-message { + &, * { + cursor: pointer; + } + } + } + + min-height: 150px; + border: 2px solid rgba(0, 0, 0, 0.3); + background: white; + padding: 20px 20px; + + &.dz-started { + .dz-message { + display: none; + } + } + + &.dz-drag-hover { + border-style: solid; + .dz-message { + opacity: 0.5; + } + } + .dz-message { + text-align: center; + margin: 2em 0; + + + } + + + + .dz-preview { + position: relative; + display: inline-block; + + vertical-align: top; + + margin: 16px; + min-height: 100px; + + &:hover { + // Making sure that always the hovered preview element is on top + z-index: 1000; + .dz-details { + opacity: 1; + } + } + + &.dz-file-preview { + + .dz-image { + border-radius: $image-border-radius; + background: #999; + background: linear-gradient(to bottom, #eee, #ddd); + } + + .dz-details { + opacity: 1; + } + } + + &.dz-image-preview { + background: white; + .dz-details { + @include prefix((transition: opacity 0.2s linear)); + } + } + + .dz-remove { + font-size: 14px; + text-align: center; + display: block; + cursor: pointer; + border: none; + &:hover { + text-decoration: underline; + } + } + + &:hover .dz-details { + opacity: 1; + } + .dz-details { + $background-color: #444; + + z-index: 20; + + position: absolute; + top: 0; + left: 0; + + opacity: 0; + + font-size: 13px; + min-width: 100%; + max-width: 100%; + padding: 2em 1em; + text-align: center; + color: rgba(0, 0, 0, 0.9); + + $width: 120px; + + line-height: 150%; + + .dz-size { + margin-bottom: 1em; + font-size: 16px; + } + + .dz-filename { + + white-space: nowrap; + + &:hover { + span { + border: 1px solid rgba(200, 200, 200, 0.8); + background-color: rgba(255, 255, 255, 0.8); + } + } + &:not(:hover) { + span { + border: 1px solid transparent; + } + overflow: hidden; + text-overflow: ellipsis; + } + + } + + .dz-filename, .dz-size { + span { + background-color: rgba(255, 255, 255, 0.4); + padding: 0 0.4em; + border-radius: 3px; + } + } + + } + + &:hover { + .dz-image { + // opacity: 0.8; + img { + @include prefix((transform: scale(1.05, 1.05))); // Getting rid of that white bleed-in + @include prefix((filter: blur(8px)), webkit); // Getting rid of that white bleed-in + } + } + } + .dz-image { + border-radius: $image-border-radius; + overflow: hidden; + width: $image-size; + height: $image-size; + position: relative; + display: block; + z-index: 10; + + img { + display: block; + } + } + + + &.dz-success { + .dz-success-mark { + @include prefix((animation: passing-through 3s cubic-bezier(0.770, 0.000, 0.175, 1.000))); + } + } + &.dz-error { + .dz-error-mark { + opacity: 1; + @include prefix((animation: slide-in 3s cubic-bezier(0.770, 0.000, 0.175, 1.000))); + } + } + + + .dz-success-mark, .dz-error-mark { + + $image-height: 54px; + $image-width: 54px; + + pointer-events: none; + + opacity: 0; + z-index: 500; + + position: absolute; + display: block; + top: 50%; + left: 50%; + margin-left: -($image-width/2); + margin-top: -($image-height/2); + + svg { + display: block; + width: $image-width; + height: $image-height; + } + } + + + &.dz-processing .dz-progress { + opacity: 1; + @include prefix((transition: all 0.2s linear)); + } + &.dz-complete .dz-progress { + opacity: 0; + @include prefix((transition: opacity 0.4s ease-in)); + } + + &:not(.dz-processing) { + .dz-progress { + @include prefix((animation: pulse 6s ease infinite)); + } + } + .dz-progress { + + opacity: 1; + z-index: 1000; + + pointer-events: none; + position: absolute; + height: 16px; + left: 50%; + top: 50%; + margin-top: -8px; + + width: 80px; + margin-left: -40px; + + // border: 2px solid #333; + background: rgba(255, 255, 255, 0.9); + + // Fix for chrome bug: https://code.google.com/p/chromium/issues/detail?id=157218 + -webkit-transform: scale(1); + + + border-radius: 8px; + + overflow: hidden; + + .dz-upload { + background: #333; + background: linear-gradient(to bottom, #666, #444); + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 0; + @include prefix((transition: width 300ms ease-in-out)); + } + + } + + &.dz-error { + .dz-error-message { + display: block; + } + &:hover .dz-error-message { + opacity: 1; + pointer-events: auto; + } + } + + .dz-error-message { + $width: $image-size + 20px; + $color: rgb(190, 38, 38); + + pointer-events: none; + z-index: 1000; + position: absolute; + display: block; + display: none; + opacity: 0; + @include prefix((transition: opacity 0.3s ease)); + border-radius: 8px; + font-size: 13px; + top: $image-size + 10px; + left: -10px; + width: $width; + background: $color; + background: linear-gradient(to bottom, $color, darken($color, 5%)); + padding: 0.5em 1.2em; + color: white; + + // The triangle pointing up + &:after { + content: ''; + position: absolute; + top: -6px; + left: $width / 2 - 6px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid $color; + } + } + + } +} + + diff --git a/examples/demo/site/modules/files/files/scss/files.scss b/examples/demo/site/modules/files/files/scss/files.scss new file mode 100644 index 0000000..836fcfd --- /dev/null +++ b/examples/demo/site/modules/files/files/scss/files.scss @@ -0,0 +1,63 @@ +.uploaded-files { + table { + width: 100%; + border-collapse: collapse; + border: 1px solid black; + + th { + padding: 3px 0 3px 5px; + } + + td { + padding: 3px 0 3px 5px; + } + } + + a.button{ + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; + &:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; + } + } +} + +.upload-files { + .center { + text-align: center; + padding: 10px; + } + a.button{ + margin: auto; + width: 100px; + + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; + &:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; + } + } +} + + +/******************* Drop Zone *******************/ + +.dropzone { + width: 100%; + border: 2px dashed blue; + border-radius: 3px; + text-align: center; + padding-top: 15px; + padding-bottom: 15px; +} + diff --git a/examples/demo/src/demo_cms_execution.e b/examples/demo/src/demo_cms_execution.e index 5c37f2d..d4d1384 100644 --- a/examples/demo/src/demo_cms_execution.e +++ b/examples/demo/src/demo_cms_execution.e @@ -57,6 +57,9 @@ feature -- CMS modules a_setup.register_module (create {CMS_NODE_MODULE}.make (a_setup)) a_setup.register_module (create {CMS_BLOG_MODULE}.make) + -- Files + a_setup.register_module (create {CMS_FILES_MODULE}.make) + -- Contact a_setup.register_module (create {CMS_CONTACT_MODULE}.make) @@ -76,6 +79,7 @@ feature -- CMS modules a_setup.register_module (create {GOOGLE_CUSTOM_SEARCH_MODULE}.make) a_setup.register_module (create {CMS_DEBUG_MODULE}.make) a_setup.register_module (create {CMS_DEMO_MODULE}.make) + end end diff --git a/modules/files/cms_file.e b/modules/files/cms_file.e new file mode 100644 index 0000000..f762b03 --- /dev/null +++ b/modules/files/cms_file.e @@ -0,0 +1,69 @@ +note + description: "Interface representing any files under `{CMS_API}.files_location' ." + date: "$Date$" + revision: "$Revision$" + +class + CMS_FILE + +create + make + +feature {NONE} -- Initializaion + + make (a_relative_path: PATH; a_api: CMS_API) + do + cms_api := a_api + relative_path := a_relative_path + end + + cms_api: CMS_API + +feature -- Access + + filename: STRING_32 + -- File name of Current file. + local + p: PATH + do + p := relative_path + if attached p.entry as e then + Result := e.name + else + Result := p.name + end + end + + relative_path: PATH + -- Path relative the `CMS_API.files_location'. + + owner: detachable CMS_USER + -- Optional owner. + +feature -- Status report + + is_directory: BOOLEAN + local + d: DIRECTORY + do + create d.make_with_path (cms_api.files_location.extended_path (relative_path)) + Result := d.exists + end + + is_file: BOOLEAN + local + f: RAW_FILE + do + create f.make_with_path (cms_api.files_location.extended_path (relative_path)) + Result := f.exists + end + +feature -- Element change + + set_owner (u: detachable CMS_USER) + -- Set `owner' to `u'. + do + owner := u + end + +end diff --git a/modules/files/cms_file_metadata.e b/modules/files/cms_file_metadata.e new file mode 100644 index 0000000..1a349d2 --- /dev/null +++ b/modules/files/cms_file_metadata.e @@ -0,0 +1,57 @@ +note + description: "Metadata associated to a CMS_FILE." + date: "$Date$" + revision: "$Revision$" + +class + CMS_FILE_METADATA + +create + make, + make_empty + +feature {NONE} -- Initialization + + make_empty + do + create date.make_now_utc + end + + make (f: CMS_FILE) + do + make_empty + end + +feature -- Access + + user: detachable CMS_USER + + date: detachable DATE_TIME + + size: INTEGER + + file_type: detachable READABLE_STRING_8 + +feature -- Element change + + set_user (u: detachable CMS_USER) + do + user := u + end + + set_date (dt: detachable DATE_TIME) + do + date := dt + end + + set_size (a_size: INTEGER) + do + size := a_size + end + + set_file_type (a_type: detachable READABLE_STRING_8) + do + file_type := a_type + end + +end diff --git a/modules/files/cms_files_api.e b/modules/files/cms_files_api.e new file mode 100644 index 0000000..1ecbcc4 --- /dev/null +++ b/modules/files/cms_files_api.e @@ -0,0 +1,272 @@ +note + description: "API to manage files." + date: "$Date$" + revision: "$Revision$" + +class + CMS_FILES_API + +inherit + CMS_MODULE_API + + REFACTORING_HELPER + +create + make + +feature -- Access : path + + uploads_relative_path: PATH + -- Path relative to `{CMS_API}.files_location'. + do + create Result.make_from_string (uploads_directory_name) + end + + uploads_directory: PATH + do + Result := cms_api.files_location.extended (uploads_directory_name) + end + + uploaded_file_path (f: READABLE_STRING_GENERAL): PATH + do + Result := uploads_directory.extended (f) + end + + thumbnail_directory: PATH + do + Result := uploads_directory.extended (thumbnail_directory_name) + end + +feature {CMS_FILES_MODULE} -- Access : metadata path + + metadata_path (f: READABLE_STRING_GENERAL): PATH + do + Result := metadata_directory.extended (f).appended_with_extension ("cms-metadata") + end + + metadata_directory: PATH + do + Result := uploads_directory.extended (metadata_directory_name) + end + +feature -- Access : links + + file_link (f: CMS_FILE): CMS_LOCAL_LINK + local + s: STRING + do + s := "files" + across + f.relative_path.components as ic + loop + s.append_character ('/') + s.append (percent_encoded (ic.item.name)) + end + create Result.make (f.filename, s) + end + +feature {NONE} -- Constants + + uploads_directory_name: STRING = "uploads" + + metadata_directory_name: STRING = ".metadata" + + thumbnail_directory_name: STRING = ".thumbnails" + +feature -- Factory + + new_file (a_relative_path: PATH): CMS_FILE + -- New CMS_FILE for path `a_relative_path' relative to `files' directory. + do + create Result.make (a_relative_path, cms_api) + end + + new_uploads_file (p: PATH): CMS_FILE + -- New uploaded path from `p' relative to `uploads_directory'. + do + create Result.make (uploads_relative_path.extended_path (p), cms_api) + end + +feature -- Storage + + delete_file (fn: READABLE_STRING_GENERAL) + -- Delete file at `fn'. + local + p: PATH + do + error_handler.reset + p := uploaded_file_path (fn) + safe_delete (p) + if not has_error then + p := metadata_path (fn) + safe_delete (p) + end + end + + save_uploaded_file (uf: CMS_UPLOADED_FILE) + local + p: PATH + ut: FILE_UTILITIES + stored: BOOLEAN + original_name: STRING_32 + n: INTEGER_32 + finished: BOOLEAN + do + reset_error + create original_name.make_from_string (uf.filename) + + p := uf.location + if not p.is_absolute then + p := uploads_directory.extended_path (p) + end + + if ut.file_path_exists (p) then + + from + n := 1 + until + finished + loop + if ut.file_path_exists (p) then + uf.set_new_location_with_number (n) + p := uf.location + if p.is_absolute then + else + p := uploads_directory.extended_path (p) + end + n := n + 1 + else + finished := True + end + end + stored := uf.move_to (p) + else + -- move file to path + stored := uf.move_to (p) + end + + if stored then + save_metadata_from_uploaded_file (uf, cms_api.user) + else + error_handler.add_custom_error (-1, "uploaded file storage failed", "Issue occurred when saving uploaded file!") + end + end + + save_metadata_from_uploaded_file (a_uploaded_file: CMS_UPLOADED_FILE; u: detachable CMS_USER) + local + f: detachable RAW_FILE + h_date: HTTP_DATE + retried: BOOLEAN + do + if retried then + -- FIXME: Report error? + if f /= Void and then not f.is_closed then + f.close + end + else + -- create a file for metadata + create f.make_with_path (metadata_path (a_uploaded_file.filename)) + + if f.exists then + f.open_write + else + f.create_read_write + end + -- insert username + if u /= Void then + f.put_string (u.id.out) + f.put_new_line +-- f.put_string (utf.utf_32_string_to_utf_8_string_8 (u.name)) +-- f.put_new_line + else + f.put_new_line + f.put_new_line + end + -- insert uploaded_time + create h_date.make_now_utc + f.put_string (h_date.timestamp.out) + f.put_new_line + + -- insert size of file + f.put_string (a_uploaded_file.size.out) + f.put_new_line + + -- insert file type + if attached a_uploaded_file.type as type then + f.put_string (type.out) + f.put_new_line + end + + f.close + end + rescue + retried := True + retry + end + + metadata (a_cms_file: CMS_FILE): detachable CMS_FILE_METADATA + local + f: RAW_FILE + s: READABLE_STRING_8 + do + if attached metadata_path (a_cms_file.filename) as p then + create f.make_with_path (p) + if f.exists and then f.is_access_readable then + create Result.make_empty + + f.open_read + + f.read_line + s := f.last_string + if s.is_integer_64 then + Result.set_user (cms_api.user_api.user_by_id (s.to_integer_64)) + else + Result.set_user (cms_api.user_api.user_by_name (s)) + end + + f.read_line + s := f.last_string + if s.is_integer_64 then + Result.set_date ((create {HTTP_DATE}.make_from_timestamp (s.to_integer_64)).date_time) + end + + f.read_line + s := f.last_string + if s.is_integer_32 then + Result.set_size (s.to_integer_32) + else + Result.set_size (-1) + end + + if not f.end_of_file then + f.read_line + Result.set_file_type (f.last_string) + end + f.close + end + end + end + + safe_delete (p: PATH) + -- Safe remove file at path `p'. + local + f: RAW_FILE + retried: BOOLEAN + do + if retried then + error_handler.add_custom_error (-1, "Can not delete file", {STRING_32} "Can not delete file %"" + p.name + "%"") + else + create f.make_with_path (p) + if f.exists then + f.delete + else + -- Not considered as failure. + end + end + rescue + retried := True + retry + end + + +end diff --git a/modules/files/cms_files_module.e b/modules/files/cms_files_module.e new file mode 100644 index 0000000..b2a1c46 --- /dev/null +++ b/modules/files/cms_files_module.e @@ -0,0 +1,484 @@ +note + description: "files module." + date: "$Date$" + revision: "$Revision$" + +class + CMS_FILES_MODULE + +inherit + CMS_MODULE + rename + module_api as files_api + redefine + install, + initialize, + setup_hooks, + permissions, + files_api + end + + CMS_HOOK_MENU_SYSTEM_ALTER + + SHARED_EXECUTION_ENVIRONMENT + +create + make + +feature {NONE} -- Initialization + + make + do + name := "files" + version := "1.0" + description := "Service to upload files, and manage them." + package := "file" + end + +feature -- Access + + name: STRING + + permissions: LIST [READABLE_STRING_8] + -- List of permission ids, used by this module, and declared. + do + Result := Precursor + Result.force ("admin files") + Result.force ("upload files") + end + +feature {CMS_API} -- Module Initialization + + initialize (api: CMS_API) + -- + do + Precursor (api) + if files_api = Void then + create files_api.make (api) + end + end + +feature {CMS_API}-- Module management + + install (api: CMS_API) + -- install the module + local + l_files_api: like files_api + d: DIRECTORY + do + create l_files_api.make (api) + create d.make_with_path (l_files_api.uploads_directory) + if not d.exists then + d.recursive_create_dir + end + create d.make_with_path (l_files_api.metadata_directory) + if not d.exists then + d.recursive_create_dir + end + + files_api := l_files_api + Precursor (api) + end + +feature {CMS_API} -- Access: API + + files_api: detachable CMS_FILES_API + -- + +feature -- Access: router + + setup_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- + do + map_uri_template_agent (a_router, "/" + uploads_location, agent execute_upload (?, ?, a_api), Void) -- Accepts any method GET, HEAD, POST, PUT, DELETE, ... + map_uri_template_agent (a_router, "/" + uploads_location + "{filename}", agent display_uploaded_file_info (?, ?, a_api), a_router.methods_get) + map_uri_template_agent (a_router, "/" + uploads_location + "remove/{filename}", agent remove (?, ?, a_api), a_router.methods_get) + end + + uploads_location: STRING = "upload/" + +feature -- Hooks + + setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER) + do + a_hooks.subscribe_to_menu_system_alter_hook (Current) + end + + menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE) + local + link: CMS_LOCAL_LINK + do + -- login in demo did somehow not work + if a_response.has_permission ("upload files") then + create link.make ("Upload files", uploads_location) + a_menu_system.navigation_menu.extend (link) + end + end + +feature -- Handler + + execute_not_found_handler (uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE) + -- `uri' is not found, redirect to default page + do + res.redirect_now_with_content (req.script_url ("/"), uri + ": not found. %N Redirection to" + req.script_url ("/"), "text/html") + end + + display_uploaded_file_info (req: WSF_REQUEST; res: WSF_RESPONSE; api: CMS_API) + -- Display information related to a cms uploaded file. + local + body: STRING_8 + r: CMS_RESPONSE + f: CMS_FILE + md: detachable CMS_FILE_METADATA + fn: READABLE_STRING_32 + do + check req.is_get_request_method end + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + + -- add style + r.add_style (r.url ("/module/" + name + "/files/css/files.css", Void), Void) + + create body.make_empty + if attached {WSF_STRING} req.path_parameter ("filename") as p_filename then + fn := p_filename.value + r.set_page_title ({STRING_32} "File %"" + fn + {STRING_32} "%"") + body.append ("
%N") -- To ease css customization. + if attached files_api as l_files_api then + f := l_files_api.new_uploads_file (create {PATH}.make_from_string (fn)) + + body.append ("
") + + md := l_files_api.metadata (f) + + --| Uploader user + body.append ("User: ") + if md /= Void and then attached md.user as meta_user then + body.append (api.html_encoded (meta_user.name)) + else + body.append ("unknown user") + end + body.append ("
%N") + + --| Uploaded date + body.append ("Upload Time: ") + if md /= Void and then attached md.date as meta_time then + body.append (meta_time.out) + else + body.append ("NA") + end + body.append ("
%N") + + --| File size + body.append (" File Size: ") + if md /= Void and then md.size > 0 then + body.append (file_size_human_string (md.size)) + else + body.append ("NA") + end + body.append ("
%N") + + --| File type + body.append ("File Type: ") + if md /= Void and then attached md.file_type as meta_type then + body.append (meta_type) + else + body.append ("NA") + end + body.append ("

%N") + + body.append ("Download%N") + body.append ("Remove%N") + body.append ("
%N") -- metadata + + body.append ("
") + if + attached f.relative_path.extension as ext and then + ( + ext.is_case_insensitive_equal_general ("png") + or ext.is_case_insensitive_equal_general ("jpg") + or ext.is_case_insensitive_equal_general ("gif") + ) + then + body.append ("") + else + -- add default thumbnail + body.append ("") + end + body.append ("
%N") -- Overview + end + body.append ("
%N") + end + r.add_to_primary_tabs (create {CMS_LOCAL_LINK}.make ("Uploaded files", uploads_location)) + r.set_main_content (body) + r.execute + end + + execute_upload (req: WSF_REQUEST; res: WSF_RESPONSE; api: CMS_API) + local + body: STRING_8 + r: CMS_RESPONSE + do + if req.is_get_head_request_method or req.is_post_request_method then + create body.make_empty + body.append ("

Upload files

%N") + + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + + -- set style + r.add_style (r.url ("/module/" + name + "/files/css/files.css", void), void) + + -- add JS for dropzone + r.add_javascript_url (r.url ("/module/" + name + "/files/js/dropzone.js", void)) + r.add_style (r.url ("/module/" + name + "/files/js/dropzone.css", void), void) + + if r.has_permission ("upload files") then + -- create body + body.append ("

Please choose some file(s) to upload.

") + + -- create form to choose files and upload them + body.append ("
") + body.append ("
") + body.append ("
%N") + body.append ("") + body.append ("
") + if req.is_post_request_method then + process_uploaded_files (req, api, body) + end + else + create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api) + end + + -- Build the response. + + append_uploaded_file_album_to (req, api, body) + + r.set_main_content (body) + else + create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api) + end + r.execute + end + + process_uploaded_files (req: WSF_REQUEST; api: CMS_API; a_output: STRING) + -- show all newly uploaded files + local + l_uploaded_file: CMS_UPLOADED_FILE + uf: WSF_UPLOADED_FILE + do + if attached files_api as l_files_api then + -- if has uploaded files, then store them + if req.has_uploaded_file then + a_output.append ("Newly uploaded file(s): %N") + a_output.append ("
    ") + across + req.uploaded_files as ic + loop + uf := ic.item + create l_uploaded_file.make_with_uploaded_file (l_files_api.uploads_directory, uf) + a_output.append ("
  • ") + a_output.append (api.html_encoded (l_uploaded_file.filename)) + + -- create medadata file + l_uploaded_file.set_size (uf.size) + l_uploaded_file.set_type (uf.content_type) + + -- store the just uploaded file + l_files_api.save_uploaded_file (l_uploaded_file) + + if l_files_api.has_error then + a_output.append (" : upload failed!") + end + a_output.append ("
  • ") + end + a_output.append ("
%N") + end + end + end + + append_uploaded_file_album_to (req: WSF_REQUEST; api: CMS_API; a_output: STRING) + local + d: DIRECTORY + f: CMS_FILE + p: PATH + rel: PATH + md: detachable CMS_FILE_METADATA + do + if attached files_api as l_files_api then + rel := l_files_api.uploads_relative_path + p := l_files_api.uploads_directory + + a_output.append ("
%N") + a_output.append ("Uploaded files:%N") + a_output.append ("%N") + a_output.append ("%N") + + create d.make_with_path (p) + if d.exists then + across + d.entries as ic + loop + + if ic.item.is_current_symbol then + -- Ignore + elseif ic.item.is_parent_symbol then + -- Ignore for now. + else + f := l_files_api.new_file (rel.extended_path (ic.item)) + + -- check if f is directory -> yes, then do not show + if not f.is_directory then + a_output.append ("") + + -- add filename + a_output.append ("%N") + + md := l_files_api.metadata (f) + if md = Void then + a_output.append ("%N") + a_output.append ("%N") + a_output.append ("%N") + else + + -- add uploading time + a_output.append ("%N") + + -- add user + a_output.append ("%N") + + -- add size + a_output.append ("%N") + end + + -- add download link + a_output.append ("%N") + + -- add remove button + a_output.append ("%N") + + a_output.append ("%N") + else + if f.relative_path.is_current_symbol or f.relative_path.is_parent_symbol then + -- Ignore "." and ".." + else + + -- folder support not yet supported + +-- -- add directory identifier +-- a_output.append ("%N") + +-- a_output.append ("%N") + +-- a_output.append ("") + end + + end + end + end + end + a_output.append ("
FilenameUploading TimeUserSize
") + a_output.append ("") + a_output.append (api.html_encoded (f.filename)) + a_output.append ("") + a_output.append ("") + if attached md.date as meta_time then + a_output.append (meta_time.out) + end + a_output.append ("") + if attached md.user as u then + a_output.append (api.html_encoded (u.name)) + end + a_output.append ("") + if md.size > 0 then + a_output.append (file_size_human_string (md.size)) + else + a_output.append ("NA") + end + a_output.append ("") + a_output.append ("Download%N") + a_output.append ("") + a_output.append ("Remove%N") + a_output.append ("
[dir]") +-- a_output.append ("") +-- a_output.append (api.html_encoded (f.filename)) +-- a_output.append ("") +-- a_output.append ("
%N") + a_output.append ("
%N") + end + end + + remove (req: WSF_REQUEST; res: WSF_RESPONSE; api: CMS_API) + local + body: STRING + r: CMS_RESPONSE + err: BOOLEAN + do + + if attached files_api as l_files_api then + if attached {WSF_STRING} req.path_parameter ("filename") as p_filename then + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + + l_files_api.delete_file (p_filename.value) + err := l_files_api.has_error + l_files_api.reset_error + create body.make_empty + + if err then + body.append ("

The file has been removed successfully!

") + else + body.append ("

The file removal failed!

") + end + + r.add_to_primary_tabs (create {CMS_LOCAL_LINK}.make ("Uploaded files", uploads_location)) + r.set_main_content (body) + else + create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api) + r.add_error_message ("Missing 'filename' parameter!") + end + else + create {INTERNAL_SERVER_ERROR_CMS_RESPONSE} r.make (req, res, api) + r.set_main_content ("Removal of file failed due to internal server error!") + end + r.execute + end + +feature -- Helpers + + file_size_human_string (a_size: INTEGER): STRING + local + size: INTEGER + do + size := a_size + if size >= 1000000 then + size := size // 1000000 + Result := size.out + " MB" + else + if size >= 1000 then + size := size // 1000 + Result := size.out + " kB" + else + Result := size.out + " bytes" + end + end + end + +feature -- Mapping helper: uri template agent (analogue to the demo-module) + + map_uri_template (a_router: WSF_ROUTER; a_tpl: STRING; h: WSF_URI_TEMPLATE_HANDLER; rqst_methods: detachable WSF_REQUEST_METHODS) + -- Map `h' as handler for `a_tpl', according to `rqst_methods'. + require + a_tpl_attached: a_tpl /= Void + h_attached: h /= Void + do + a_router.map (create {WSF_URI_TEMPLATE_MAPPING}.make (a_tpl, h), rqst_methods) + end + + map_uri_template_agent (a_router: WSF_ROUTER; a_tpl: READABLE_STRING_8; proc: PROCEDURE [TUPLE [req: WSF_REQUEST; res: WSF_RESPONSE]]; rqst_methods: detachable WSF_REQUEST_METHODS) + -- Map `proc' as handler for `a_tpl', according to `rqst_methods'. + require + a_tpl_attached: a_tpl /= Void + proc_attached: proc /= Void + do + map_uri_template (a_router, a_tpl, create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (proc), rqst_methods) + end + +end diff --git a/modules/files/cms_uploaded_file.e b/modules/files/cms_uploaded_file.e new file mode 100644 index 0000000..0273524 --- /dev/null +++ b/modules/files/cms_uploaded_file.e @@ -0,0 +1,105 @@ +note + description: "Summary description for {CMS_UPLOADED_FILE}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_UPLOADED_FILE + +create + make_with_uploaded_file + +feature {NONE} -- Initializaion + + make_with_uploaded_file (a_uploads_directory: PATH; uf: WSF_UPLOADED_FILE) + do + uploads_directory := a_uploads_directory + uploaded_file := uf + location := a_uploads_directory.extended (uf.safe_filename) + end + +feature -- Access + + uploaded_file: WSF_UPLOADED_FILE + + uploads_directory: PATH + + filename: STRING_32 + -- File name of Current file. + local + p: PATH + do + p := location + if attached p.entry as e then + Result := e.name + else + Result := p.name + end + end + + + location: PATH + -- Absolute path, or relative path to the `CMS_API.files_location'. + + owner: detachable CMS_USER + -- Optional owner. + + upload_time: detachable DATE_TIME + -- time when the file was uploaded + + size: detachable INTEGER_32 + -- file size + + type: detachable STRING + -- file type + +feature -- Element change + + set_owner (u: detachable CMS_USER) + -- Set `owner' to `u'. + do + owner := u + end + + set_time (a_time: detachable DATE_TIME) + -- Set `upload_time' to `a_time' + do + upload_time := a_time + end + + set_size (a_size: detachable INTEGER_32) + -- Set `size' to `a_size' + do + size := a_size + end + + set_type (a_type: detachable STRING) + -- Set `type' to `a_type' + do + type := a_type + end + + set_new_location_with_number (a_number: INTEGER_32) + -- sets `a_number' after the name. This is done when the file was already uploaded + local + position: INTEGER_32 + new_name: STRING_8 + do + position := uploaded_file.string_representation.index_of ('.', 1) + create new_name.make_empty + + new_name := uploaded_file.string_representation.head (position-1) + new_name.append ("_(" + a_number.out + ")") + new_name.append (uploaded_file.string_representation.substring (position, uploaded_file.string_representation.count)) + + location := uploads_directory.extended (new_name) + end + +feature -- Basic operation + + move_to (p: PATH): BOOLEAN + do + Result := uploaded_file.move_to (p.name) + end + +end diff --git a/modules/files/files-safe.ecf b/modules/files/files-safe.ecf new file mode 100644 index 0000000..14b05b8 --- /dev/null +++ b/modules/files/files-safe.ecf @@ -0,0 +1,21 @@ + + + + + + /.svn$ + /CVS$ + /EIFGENs$ + + + diff --git a/modules/files/files.ecf b/modules/files/files.ecf new file mode 100644 index 0000000..e786f4a --- /dev/null +++ b/modules/files/files.ecf @@ -0,0 +1,22 @@ + + + + + + /.svn$ + /CVS$ + /EIFGENs$ + + + + + + + + + + + + + diff --git a/modules/files/site/files/css/files.css b/modules/files/site/files/css/files.css new file mode 100644 index 0000000..d522e67 --- /dev/null +++ b/modules/files/site/files/css/files.css @@ -0,0 +1,52 @@ +.uploaded-files table { + width: 100%; + border-collapse: collapse; + border: 1px solid black; +} +.uploaded-files table th { + padding: 3px 0 3px 5px; +} +.uploaded-files table td { + padding: 3px 0 3px 5px; +} +.uploaded-files a.button { + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; +} +.uploaded-files a.button:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; +} + +.upload-files .center { + text-align: center; + padding: 10px; +} +.upload-files a.button { + margin: auto; + width: 100px; + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; +} +.upload-files a.button:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; +} + +/******************* Drop Zone *******************/ +.dropzone { + width: 100%; + border: 2px dashed blue; + border-radius: 3px; + text-align: center; + padding-top: 15px; + padding-bottom: 15px; +} diff --git a/modules/files/site/files/img/file-logo.png b/modules/files/site/files/img/file-logo.png new file mode 100644 index 0000000..dbe20a7 Binary files /dev/null and b/modules/files/site/files/img/file-logo.png differ diff --git a/modules/files/site/files/js/dropzone.css b/modules/files/site/files/js/dropzone.css new file mode 100644 index 0000000..d04515e --- /dev/null +++ b/modules/files/site/files/js/dropzone.css @@ -0,0 +1 @@ +@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,0.3);background:white;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:0.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:white}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-ms-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,0.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,0.8);background-color:rgba(255,255,255,0.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,0.4);padding:0 0.4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05, 1.05);-moz-transform:scale(1.05, 1.05);-ms-transform:scale(1.05, 1.05);-o-transform:scale(1.05, 1.05);transform:scale(1.05, 1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;-o-transition:all 0.2s linear;transition:all 0.2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity 0.4s ease-in;-moz-transition:opacity 0.4s ease-in;-ms-transition:opacity 0.4s ease-in;-o-transition:opacity 0.4s ease-in;transition:opacity 0.4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,0.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom, #666, #444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width 300ms ease-in-out;-moz-transition:width 300ms ease-in-out;-ms-transition:width 300ms ease-in-out;-o-transition:width 300ms ease-in-out;transition:width 300ms ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity 0.3s ease;-moz-transition:opacity 0.3s ease;-ms-transition:opacity 0.3s ease;-o-transition:opacity 0.3s ease;transition:opacity 0.3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom, #be2626, #a92222);padding:0.5em 1.2em;color:white}.dropzone .dz-preview .dz-error-message:after{content:'';position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626} diff --git a/modules/files/site/files/js/dropzone.js b/modules/files/site/files/js/dropzone.js new file mode 100644 index 0000000..6b72719 --- /dev/null +++ b/modules/files/site/files/js/dropzone.js @@ -0,0 +1,1763 @@ + +/* + * + * More info at [www.dropzonejs.com](http://www.dropzonejs.com) + * + * Copyright (c) 2012, Matias Meno + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +var Dropzone, Emitter, camelize, contentLoaded, detectVerticalSquash, drawImageIOSFix, noop, without, + __slice = [].slice, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + +noop = function() {}; + +Emitter = (function() { + function Emitter() {} + + Emitter.prototype.addEventListener = Emitter.prototype.on; + + Emitter.prototype.on = function(event, fn) { + this._callbacks = this._callbacks || {}; + if (!this._callbacks[event]) { + this._callbacks[event] = []; + } + this._callbacks[event].push(fn); + return this; + }; + + Emitter.prototype.emit = function() { + var args, callback, callbacks, event, _i, _len; + event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + this._callbacks = this._callbacks || {}; + callbacks = this._callbacks[event]; + if (callbacks) { + for (_i = 0, _len = callbacks.length; _i < _len; _i++) { + callback = callbacks[_i]; + callback.apply(this, args); + } + } + return this; + }; + + Emitter.prototype.removeListener = Emitter.prototype.off; + + Emitter.prototype.removeAllListeners = Emitter.prototype.off; + + Emitter.prototype.removeEventListener = Emitter.prototype.off; + + Emitter.prototype.off = function(event, fn) { + var callback, callbacks, i, _i, _len; + if (!this._callbacks || arguments.length === 0) { + this._callbacks = {}; + return this; + } + callbacks = this._callbacks[event]; + if (!callbacks) { + return this; + } + if (arguments.length === 1) { + delete this._callbacks[event]; + return this; + } + for (i = _i = 0, _len = callbacks.length; _i < _len; i = ++_i) { + callback = callbacks[i]; + if (callback === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; + }; + + return Emitter; + +})(); + +Dropzone = (function(_super) { + var extend, resolveOption; + + __extends(Dropzone, _super); + + Dropzone.prototype.Emitter = Emitter; + + + /* + This is a list of all available events you can register on a dropzone object. + + You can register an event handler like this: + + dropzone.on("dragEnter", function() { }); + */ + + Dropzone.prototype.events = ["drop", "dragstart", "dragend", "dragenter", "dragover", "dragleave", "addedfile", "addedfiles", "removedfile", "thumbnail", "error", "errormultiple", "processing", "processingmultiple", "uploadprogress", "totaluploadprogress", "sending", "sendingmultiple", "success", "successmultiple", "canceled", "canceledmultiple", "complete", "completemultiple", "reset", "maxfilesexceeded", "maxfilesreached", "queuecomplete"]; + + Dropzone.prototype.defaultOptions = { + url: null, + method: "post", + withCredentials: false, + parallelUploads: 2, + uploadMultiple: false, + maxFilesize: 256, + paramName: "file", + createImageThumbnails: true, + maxThumbnailFilesize: 10, + thumbnailWidth: 120, + thumbnailHeight: 120, + filesizeBase: 1000, + maxFiles: null, + params: {}, + clickable: true, + ignoreHiddenFiles: true, + acceptedFiles: null, + acceptedMimeTypes: null, + autoProcessQueue: true, + autoQueue: true, + addRemoveLinks: false, + previewsContainer: null, + hiddenInputContainer: "body", + capture: null, + renameFilename: null, + dictDefaultMessage: "Drop files here to upload", + dictFallbackMessage: "Your browser does not support drag'n'drop file uploads.", + dictFallbackText: "Please use the fallback form below to upload your files like in the olden days.", + dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", + dictInvalidFileType: "You can't upload files of this type.", + dictResponseError: "Server responded with {{statusCode}} code.", + dictCancelUpload: "Cancel upload", + dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?", + dictRemoveFile: "Remove file", + dictRemoveFileConfirmation: null, + dictMaxFilesExceeded: "You can not upload any more files.", + accept: function(file, done) { + return done(); + }, + init: function() { + return noop; + }, + forceFallback: false, + fallback: function() { + var child, messageElement, span, _i, _len, _ref; + this.element.className = "" + this.element.className + " dz-browser-not-supported"; + _ref = this.element.getElementsByTagName("div"); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + if (/(^| )dz-message($| )/.test(child.className)) { + messageElement = child; + child.className = "dz-message"; + continue; + } + } + if (!messageElement) { + messageElement = Dropzone.createElement("
"); + this.element.appendChild(messageElement); + } + span = messageElement.getElementsByTagName("span")[0]; + if (span) { + if (span.textContent != null) { + span.textContent = this.options.dictFallbackMessage; + } else if (span.innerText != null) { + span.innerText = this.options.dictFallbackMessage; + } + } + return this.element.appendChild(this.getFallbackForm()); + }, + resize: function(file) { + var info, srcRatio, trgRatio; + info = { + srcX: 0, + srcY: 0, + srcWidth: file.width, + srcHeight: file.height + }; + srcRatio = file.width / file.height; + info.optWidth = this.options.thumbnailWidth; + info.optHeight = this.options.thumbnailHeight; + if ((info.optWidth == null) && (info.optHeight == null)) { + info.optWidth = info.srcWidth; + info.optHeight = info.srcHeight; + } else if (info.optWidth == null) { + info.optWidth = srcRatio * info.optHeight; + } else if (info.optHeight == null) { + info.optHeight = (1 / srcRatio) * info.optWidth; + } + trgRatio = info.optWidth / info.optHeight; + if (file.height < info.optHeight || file.width < info.optWidth) { + info.trgHeight = info.srcHeight; + info.trgWidth = info.srcWidth; + } else { + if (srcRatio > trgRatio) { + info.srcHeight = file.height; + info.srcWidth = info.srcHeight * trgRatio; + } else { + info.srcWidth = file.width; + info.srcHeight = info.srcWidth / trgRatio; + } + } + info.srcX = (file.width - info.srcWidth) / 2; + info.srcY = (file.height - info.srcHeight) / 2; + return info; + }, + + /* + Those functions register themselves to the events on init and handle all + the user interface specific stuff. Overwriting them won't break the upload + but can break the way it's displayed. + You can overwrite them if you don't like the default behavior. If you just + want to add an additional event handler, register it on the dropzone object + and don't overwrite those options. + */ + drop: function(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + dragstart: noop, + dragend: function(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + dragenter: function(e) { + return this.element.classList.add("dz-drag-hover"); + }, + dragover: function(e) { + return this.element.classList.add("dz-drag-hover"); + }, + dragleave: function(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + paste: noop, + reset: function() { + return this.element.classList.remove("dz-started"); + }, + addedfile: function(file) { + var node, removeFileEvent, removeLink, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _results; + if (this.element === this.previewsContainer) { + this.element.classList.add("dz-started"); + } + if (this.previewsContainer) { + file.previewElement = Dropzone.createElement(this.options.previewTemplate.trim()); + file.previewTemplate = file.previewElement; + this.previewsContainer.appendChild(file.previewElement); + _ref = file.previewElement.querySelectorAll("[data-dz-name]"); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + node.textContent = this._renameFilename(file.name); + } + _ref1 = file.previewElement.querySelectorAll("[data-dz-size]"); + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + node = _ref1[_j]; + node.innerHTML = this.filesize(file.size); + } + if (this.options.addRemoveLinks) { + file._removeLink = Dropzone.createElement("" + this.options.dictRemoveFile + ""); + file.previewElement.appendChild(file._removeLink); + } + removeFileEvent = (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + if (file.status === Dropzone.UPLOADING) { + return Dropzone.confirm(_this.options.dictCancelUploadConfirmation, function() { + return _this.removeFile(file); + }); + } else { + if (_this.options.dictRemoveFileConfirmation) { + return Dropzone.confirm(_this.options.dictRemoveFileConfirmation, function() { + return _this.removeFile(file); + }); + } else { + return _this.removeFile(file); + } + } + }; + })(this); + _ref2 = file.previewElement.querySelectorAll("[data-dz-remove]"); + _results = []; + for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { + removeLink = _ref2[_k]; + _results.push(removeLink.addEventListener("click", removeFileEvent)); + } + return _results; + } + }, + removedfile: function(file) { + var _ref; + if (file.previewElement) { + if ((_ref = file.previewElement) != null) { + _ref.parentNode.removeChild(file.previewElement); + } + } + return this._updateMaxFilesReachedClass(); + }, + thumbnail: function(file, dataUrl) { + var thumbnailElement, _i, _len, _ref; + if (file.previewElement) { + file.previewElement.classList.remove("dz-file-preview"); + _ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]"); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + thumbnailElement = _ref[_i]; + thumbnailElement.alt = file.name; + thumbnailElement.src = dataUrl; + } + return setTimeout(((function(_this) { + return function() { + return file.previewElement.classList.add("dz-image-preview"); + }; + })(this)), 1); + } + }, + error: function(file, message) { + var node, _i, _len, _ref, _results; + if (file.previewElement) { + file.previewElement.classList.add("dz-error"); + if (typeof message !== "String" && message.error) { + message = message.error; + } + _ref = file.previewElement.querySelectorAll("[data-dz-errormessage]"); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.textContent = message); + } + return _results; + } + }, + errormultiple: noop, + processing: function(file) { + if (file.previewElement) { + file.previewElement.classList.add("dz-processing"); + if (file._removeLink) { + return file._removeLink.textContent = this.options.dictCancelUpload; + } + } + }, + processingmultiple: noop, + uploadprogress: function(file, progress, bytesSent) { + var node, _i, _len, _ref, _results; + if (file.previewElement) { + _ref = file.previewElement.querySelectorAll("[data-dz-uploadprogress]"); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + if (node.nodeName === 'PROGRESS') { + _results.push(node.value = progress); + } else { + _results.push(node.style.width = "" + progress + "%"); + } + } + return _results; + } + }, + totaluploadprogress: noop, + sending: noop, + sendingmultiple: noop, + success: function(file) { + if (file.previewElement) { + return file.previewElement.classList.add("dz-success"); + } + }, + successmultiple: noop, + canceled: function(file) { + return this.emit("error", file, "Upload canceled."); + }, + canceledmultiple: noop, + complete: function(file) { + if (file._removeLink) { + file._removeLink.textContent = this.options.dictRemoveFile; + } + if (file.previewElement) { + return file.previewElement.classList.add("dz-complete"); + } + }, + completemultiple: noop, + maxfilesexceeded: noop, + maxfilesreached: noop, + queuecomplete: noop, + addedfiles: noop, + previewTemplate: "
\n
\n
\n
\n
\n
\n
\n
\n
\n \n Check\n \n \n \n \n \n
\n
\n \n Error\n \n \n \n \n \n \n \n
\n
" + }; + + extend = function() { + var key, object, objects, target, val, _i, _len; + target = arguments[0], objects = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + for (_i = 0, _len = objects.length; _i < _len; _i++) { + object = objects[_i]; + for (key in object) { + val = object[key]; + target[key] = val; + } + } + return target; + }; + + function Dropzone(element, options) { + var elementOptions, fallback, _ref; + this.element = element; + this.version = Dropzone.version; + this.defaultOptions.previewTemplate = this.defaultOptions.previewTemplate.replace(/\n*/g, ""); + this.clickableElements = []; + this.listeners = []; + this.files = []; + if (typeof this.element === "string") { + this.element = document.querySelector(this.element); + } + if (!(this.element && (this.element.nodeType != null))) { + throw new Error("Invalid dropzone element."); + } + if (this.element.dropzone) { + throw new Error("Dropzone already attached."); + } + Dropzone.instances.push(this); + this.element.dropzone = this; + elementOptions = (_ref = Dropzone.optionsForElement(this.element)) != null ? _ref : {}; + this.options = extend({}, this.defaultOptions, elementOptions, options != null ? options : {}); + if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { + return this.options.fallback.call(this); + } + if (this.options.url == null) { + this.options.url = this.element.getAttribute("action"); + } + if (!this.options.url) { + throw new Error("No URL provided."); + } + if (this.options.acceptedFiles && this.options.acceptedMimeTypes) { + throw new Error("You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated."); + } + if (this.options.acceptedMimeTypes) { + this.options.acceptedFiles = this.options.acceptedMimeTypes; + delete this.options.acceptedMimeTypes; + } + this.options.method = this.options.method.toUpperCase(); + if ((fallback = this.getExistingFallback()) && fallback.parentNode) { + fallback.parentNode.removeChild(fallback); + } + if (this.options.previewsContainer !== false) { + if (this.options.previewsContainer) { + this.previewsContainer = Dropzone.getElement(this.options.previewsContainer, "previewsContainer"); + } else { + this.previewsContainer = this.element; + } + } + if (this.options.clickable) { + if (this.options.clickable === true) { + this.clickableElements = [this.element]; + } else { + this.clickableElements = Dropzone.getElements(this.options.clickable, "clickable"); + } + } + this.init(); + } + + Dropzone.prototype.getAcceptedFiles = function() { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.accepted) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.getRejectedFiles = function() { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (!file.accepted) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.getFilesWithStatus = function(status) { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.status === status) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.getQueuedFiles = function() { + return this.getFilesWithStatus(Dropzone.QUEUED); + }; + + Dropzone.prototype.getUploadingFiles = function() { + return this.getFilesWithStatus(Dropzone.UPLOADING); + }; + + Dropzone.prototype.getAddedFiles = function() { + return this.getFilesWithStatus(Dropzone.ADDED); + }; + + Dropzone.prototype.getActiveFiles = function() { + var file, _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.status === Dropzone.UPLOADING || file.status === Dropzone.QUEUED) { + _results.push(file); + } + } + return _results; + }; + + Dropzone.prototype.init = function() { + var eventName, noPropagation, setupHiddenFileInput, _i, _len, _ref, _ref1; + if (this.element.tagName === "form") { + this.element.setAttribute("enctype", "multipart/form-data"); + } + if (this.element.classList.contains("dropzone") && !this.element.querySelector(".dz-message")) { + this.element.appendChild(Dropzone.createElement("
" + this.options.dictDefaultMessage + "
")); + } + if (this.clickableElements.length) { + setupHiddenFileInput = (function(_this) { + return function() { + if (_this.hiddenFileInput) { + _this.hiddenFileInput.parentNode.removeChild(_this.hiddenFileInput); + } + _this.hiddenFileInput = document.createElement("input"); + _this.hiddenFileInput.setAttribute("type", "file"); + if ((_this.options.maxFiles == null) || _this.options.maxFiles > 1) { + _this.hiddenFileInput.setAttribute("multiple", "multiple"); + } + _this.hiddenFileInput.className = "dz-hidden-input"; + if (_this.options.acceptedFiles != null) { + _this.hiddenFileInput.setAttribute("accept", _this.options.acceptedFiles); + } + if (_this.options.capture != null) { + _this.hiddenFileInput.setAttribute("capture", _this.options.capture); + } + _this.hiddenFileInput.style.visibility = "hidden"; + _this.hiddenFileInput.style.position = "absolute"; + _this.hiddenFileInput.style.top = "0"; + _this.hiddenFileInput.style.left = "0"; + _this.hiddenFileInput.style.height = "0"; + _this.hiddenFileInput.style.width = "0"; + document.querySelector(_this.options.hiddenInputContainer).appendChild(_this.hiddenFileInput); + return _this.hiddenFileInput.addEventListener("change", function() { + var file, files, _i, _len; + files = _this.hiddenFileInput.files; + if (files.length) { + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + _this.addFile(file); + } + } + _this.emit("addedfiles", files); + return setupHiddenFileInput(); + }); + }; + })(this); + setupHiddenFileInput(); + } + this.URL = (_ref = window.URL) != null ? _ref : window.webkitURL; + _ref1 = this.events; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + eventName = _ref1[_i]; + this.on(eventName, this.options[eventName]); + } + this.on("uploadprogress", (function(_this) { + return function() { + return _this.updateTotalUploadProgress(); + }; + })(this)); + this.on("removedfile", (function(_this) { + return function() { + return _this.updateTotalUploadProgress(); + }; + })(this)); + this.on("canceled", (function(_this) { + return function(file) { + return _this.emit("complete", file); + }; + })(this)); + this.on("complete", (function(_this) { + return function(file) { + if (_this.getAddedFiles().length === 0 && _this.getUploadingFiles().length === 0 && _this.getQueuedFiles().length === 0) { + return setTimeout((function() { + return _this.emit("queuecomplete"); + }), 0); + } + }; + })(this)); + noPropagation = function(e) { + e.stopPropagation(); + if (e.preventDefault) { + return e.preventDefault(); + } else { + return e.returnValue = false; + } + }; + this.listeners = [ + { + element: this.element, + events: { + "dragstart": (function(_this) { + return function(e) { + return _this.emit("dragstart", e); + }; + })(this), + "dragenter": (function(_this) { + return function(e) { + noPropagation(e); + return _this.emit("dragenter", e); + }; + })(this), + "dragover": (function(_this) { + return function(e) { + var efct; + try { + efct = e.dataTransfer.effectAllowed; + } catch (_error) {} + e.dataTransfer.dropEffect = 'move' === efct || 'linkMove' === efct ? 'move' : 'copy'; + noPropagation(e); + return _this.emit("dragover", e); + }; + })(this), + "dragleave": (function(_this) { + return function(e) { + return _this.emit("dragleave", e); + }; + })(this), + "drop": (function(_this) { + return function(e) { + noPropagation(e); + return _this.drop(e); + }; + })(this), + "dragend": (function(_this) { + return function(e) { + return _this.emit("dragend", e); + }; + })(this) + } + } + ]; + this.clickableElements.forEach((function(_this) { + return function(clickableElement) { + return _this.listeners.push({ + element: clickableElement, + events: { + "click": function(evt) { + if ((clickableElement !== _this.element) || (evt.target === _this.element || Dropzone.elementInside(evt.target, _this.element.querySelector(".dz-message")))) { + _this.hiddenFileInput.click(); + } + return true; + } + } + }); + }; + })(this)); + this.enable(); + return this.options.init.call(this); + }; + + Dropzone.prototype.destroy = function() { + var _ref; + this.disable(); + this.removeAllFiles(true); + if ((_ref = this.hiddenFileInput) != null ? _ref.parentNode : void 0) { + this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput); + this.hiddenFileInput = null; + } + delete this.element.dropzone; + return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); + }; + + Dropzone.prototype.updateTotalUploadProgress = function() { + var activeFiles, file, totalBytes, totalBytesSent, totalUploadProgress, _i, _len, _ref; + totalBytesSent = 0; + totalBytes = 0; + activeFiles = this.getActiveFiles(); + if (activeFiles.length) { + _ref = this.getActiveFiles(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + totalBytesSent += file.upload.bytesSent; + totalBytes += file.upload.total; + } + totalUploadProgress = 100 * totalBytesSent / totalBytes; + } else { + totalUploadProgress = 100; + } + return this.emit("totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent); + }; + + Dropzone.prototype._getParamName = function(n) { + if (typeof this.options.paramName === "function") { + return this.options.paramName(n); + } else { + return "" + this.options.paramName + (this.options.uploadMultiple ? "[" + n + "]" : ""); + } + }; + + Dropzone.prototype._renameFilename = function(name) { + if (typeof this.options.renameFilename !== "function") { + return name; + } + return this.options.renameFilename(name); + }; + + Dropzone.prototype.getFallbackForm = function() { + var existingFallback, fields, fieldsString, form; + if (existingFallback = this.getExistingFallback()) { + return existingFallback; + } + fieldsString = "
"; + if (this.options.dictFallbackText) { + fieldsString += "

" + this.options.dictFallbackText + "

"; + } + fieldsString += "
"; + fields = Dropzone.createElement(fieldsString); + if (this.element.tagName !== "FORM") { + form = Dropzone.createElement("
"); + form.appendChild(fields); + } else { + this.element.setAttribute("enctype", "multipart/form-data"); + this.element.setAttribute("method", this.options.method); + } + return form != null ? form : fields; + }; + + Dropzone.prototype.getExistingFallback = function() { + var fallback, getFallback, tagName, _i, _len, _ref; + getFallback = function(elements) { + var el, _i, _len; + for (_i = 0, _len = elements.length; _i < _len; _i++) { + el = elements[_i]; + if (/(^| )fallback($| )/.test(el.className)) { + return el; + } + } + }; + _ref = ["div", "form"]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + tagName = _ref[_i]; + if (fallback = getFallback(this.element.getElementsByTagName(tagName))) { + return fallback; + } + } + }; + + Dropzone.prototype.setupEventListeners = function() { + var elementListeners, event, listener, _i, _len, _ref, _results; + _ref = this.listeners; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + elementListeners = _ref[_i]; + _results.push((function() { + var _ref1, _results1; + _ref1 = elementListeners.events; + _results1 = []; + for (event in _ref1) { + listener = _ref1[event]; + _results1.push(elementListeners.element.addEventListener(event, listener, false)); + } + return _results1; + })()); + } + return _results; + }; + + Dropzone.prototype.removeEventListeners = function() { + var elementListeners, event, listener, _i, _len, _ref, _results; + _ref = this.listeners; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + elementListeners = _ref[_i]; + _results.push((function() { + var _ref1, _results1; + _ref1 = elementListeners.events; + _results1 = []; + for (event in _ref1) { + listener = _ref1[event]; + _results1.push(elementListeners.element.removeEventListener(event, listener, false)); + } + return _results1; + })()); + } + return _results; + }; + + Dropzone.prototype.disable = function() { + var file, _i, _len, _ref, _results; + this.clickableElements.forEach(function(element) { + return element.classList.remove("dz-clickable"); + }); + this.removeEventListeners(); + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + _results.push(this.cancelUpload(file)); + } + return _results; + }; + + Dropzone.prototype.enable = function() { + this.clickableElements.forEach(function(element) { + return element.classList.add("dz-clickable"); + }); + return this.setupEventListeners(); + }; + + Dropzone.prototype.filesize = function(size) { + var cutoff, i, selectedSize, selectedUnit, unit, units, _i, _len; + selectedSize = 0; + selectedUnit = "b"; + if (size > 0) { + units = ['TB', 'GB', 'MB', 'KB', 'b']; + for (i = _i = 0, _len = units.length; _i < _len; i = ++_i) { + unit = units[i]; + cutoff = Math.pow(this.options.filesizeBase, 4 - i) / 10; + if (size >= cutoff) { + selectedSize = size / Math.pow(this.options.filesizeBase, 4 - i); + selectedUnit = unit; + break; + } + } + selectedSize = Math.round(10 * selectedSize) / 10; + } + return "" + selectedSize + " " + selectedUnit; + }; + + Dropzone.prototype._updateMaxFilesReachedClass = function() { + if ((this.options.maxFiles != null) && this.getAcceptedFiles().length >= this.options.maxFiles) { + if (this.getAcceptedFiles().length === this.options.maxFiles) { + this.emit('maxfilesreached', this.files); + } + return this.element.classList.add("dz-max-files-reached"); + } else { + return this.element.classList.remove("dz-max-files-reached"); + } + }; + + Dropzone.prototype.drop = function(e) { + var files, items; + if (!e.dataTransfer) { + return; + } + this.emit("drop", e); + files = e.dataTransfer.files; + this.emit("addedfiles", files); + if (files.length) { + items = e.dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry != null)) { + this._addFilesFromItems(items); + } else { + this.handleFiles(files); + } + } + }; + + Dropzone.prototype.paste = function(e) { + var items, _ref; + if ((e != null ? (_ref = e.clipboardData) != null ? _ref.items : void 0 : void 0) == null) { + return; + } + this.emit("paste", e); + items = e.clipboardData.items; + if (items.length) { + return this._addFilesFromItems(items); + } + }; + + Dropzone.prototype.handleFiles = function(files) { + var file, _i, _len, _results; + _results = []; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + _results.push(this.addFile(file)); + } + return _results; + }; + + Dropzone.prototype._addFilesFromItems = function(items) { + var entry, item, _i, _len, _results; + _results = []; + for (_i = 0, _len = items.length; _i < _len; _i++) { + item = items[_i]; + if ((item.webkitGetAsEntry != null) && (entry = item.webkitGetAsEntry())) { + if (entry.isFile) { + _results.push(this.addFile(item.getAsFile())); + } else if (entry.isDirectory) { + _results.push(this._addFilesFromDirectory(entry, entry.name)); + } else { + _results.push(void 0); + } + } else if (item.getAsFile != null) { + if ((item.kind == null) || item.kind === "file") { + _results.push(this.addFile(item.getAsFile())); + } else { + _results.push(void 0); + } + } else { + _results.push(void 0); + } + } + return _results; + }; + + Dropzone.prototype._addFilesFromDirectory = function(directory, path) { + var dirReader, errorHandler, readEntries; + dirReader = directory.createReader(); + errorHandler = function(error) { + return typeof console !== "undefined" && console !== null ? typeof console.log === "function" ? console.log(error) : void 0 : void 0; + }; + readEntries = (function(_this) { + return function() { + return dirReader.readEntries(function(entries) { + var entry, _i, _len; + if (entries.length > 0) { + for (_i = 0, _len = entries.length; _i < _len; _i++) { + entry = entries[_i]; + if (entry.isFile) { + entry.file(function(file) { + if (_this.options.ignoreHiddenFiles && file.name.substring(0, 1) === '.') { + return; + } + file.fullPath = "" + path + "/" + file.name; + return _this.addFile(file); + }); + } else if (entry.isDirectory) { + _this._addFilesFromDirectory(entry, "" + path + "/" + entry.name); + } + } + readEntries(); + } + return null; + }, errorHandler); + }; + })(this); + return readEntries(); + }; + + Dropzone.prototype.accept = function(file, done) { + if (file.size > this.options.maxFilesize * 1024 * 1024) { + return done(this.options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", this.options.maxFilesize)); + } else if (!Dropzone.isValidFile(file, this.options.acceptedFiles)) { + return done(this.options.dictInvalidFileType); + } else if ((this.options.maxFiles != null) && this.getAcceptedFiles().length >= this.options.maxFiles) { + done(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}", this.options.maxFiles)); + return this.emit("maxfilesexceeded", file); + } else { + return this.options.accept.call(this, file, done); + } + }; + + Dropzone.prototype.addFile = function(file) { + file.upload = { + progress: 0, + total: file.size, + bytesSent: 0 + }; + this.files.push(file); + file.status = Dropzone.ADDED; + this.emit("addedfile", file); + this._enqueueThumbnail(file); + return this.accept(file, (function(_this) { + return function(error) { + if (error) { + file.accepted = false; + _this._errorProcessing([file], error); + } else { + file.accepted = true; + if (_this.options.autoQueue) { + _this.enqueueFile(file); + } + } + return _this._updateMaxFilesReachedClass(); + }; + })(this)); + }; + + Dropzone.prototype.enqueueFiles = function(files) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + this.enqueueFile(file); + } + return null; + }; + + Dropzone.prototype.enqueueFile = function(file) { + if (file.status === Dropzone.ADDED && file.accepted === true) { + file.status = Dropzone.QUEUED; + if (this.options.autoProcessQueue) { + return setTimeout(((function(_this) { + return function() { + return _this.processQueue(); + }; + })(this)), 0); + } + } else { + throw new Error("This file can't be queued because it has already been processed or was rejected."); + } + }; + + Dropzone.prototype._thumbnailQueue = []; + + Dropzone.prototype._processingThumbnail = false; + + Dropzone.prototype._enqueueThumbnail = function(file) { + if (this.options.createImageThumbnails && file.type.match(/image.*/) && file.size <= this.options.maxThumbnailFilesize * 1024 * 1024) { + this._thumbnailQueue.push(file); + return setTimeout(((function(_this) { + return function() { + return _this._processThumbnailQueue(); + }; + })(this)), 0); + } + }; + + Dropzone.prototype._processThumbnailQueue = function() { + if (this._processingThumbnail || this._thumbnailQueue.length === 0) { + return; + } + this._processingThumbnail = true; + return this.createThumbnail(this._thumbnailQueue.shift(), (function(_this) { + return function() { + _this._processingThumbnail = false; + return _this._processThumbnailQueue(); + }; + })(this)); + }; + + Dropzone.prototype.removeFile = function(file) { + if (file.status === Dropzone.UPLOADING) { + this.cancelUpload(file); + } + this.files = without(this.files, file); + this.emit("removedfile", file); + if (this.files.length === 0) { + return this.emit("reset"); + } + }; + + Dropzone.prototype.removeAllFiles = function(cancelIfNecessary) { + var file, _i, _len, _ref; + if (cancelIfNecessary == null) { + cancelIfNecessary = false; + } + _ref = this.files.slice(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.status !== Dropzone.UPLOADING || cancelIfNecessary) { + this.removeFile(file); + } + } + return null; + }; + + Dropzone.prototype.createThumbnail = function(file, callback) { + var fileReader; + fileReader = new FileReader; + fileReader.onload = (function(_this) { + return function() { + if (file.type === "image/svg+xml") { + _this.emit("thumbnail", file, fileReader.result); + if (callback != null) { + callback(); + } + return; + } + return _this.createThumbnailFromUrl(file, fileReader.result, callback); + }; + })(this); + return fileReader.readAsDataURL(file); + }; + + Dropzone.prototype.createThumbnailFromUrl = function(file, imageUrl, callback, crossOrigin) { + var img; + img = document.createElement("img"); + if (crossOrigin) { + img.crossOrigin = crossOrigin; + } + img.onload = (function(_this) { + return function() { + var canvas, ctx, resizeInfo, thumbnail, _ref, _ref1, _ref2, _ref3; + file.width = img.width; + file.height = img.height; + resizeInfo = _this.options.resize.call(_this, file); + if (resizeInfo.trgWidth == null) { + resizeInfo.trgWidth = resizeInfo.optWidth; + } + if (resizeInfo.trgHeight == null) { + resizeInfo.trgHeight = resizeInfo.optHeight; + } + canvas = document.createElement("canvas"); + ctx = canvas.getContext("2d"); + canvas.width = resizeInfo.trgWidth; + canvas.height = resizeInfo.trgHeight; + drawImageIOSFix(ctx, img, (_ref = resizeInfo.srcX) != null ? _ref : 0, (_ref1 = resizeInfo.srcY) != null ? _ref1 : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, (_ref2 = resizeInfo.trgX) != null ? _ref2 : 0, (_ref3 = resizeInfo.trgY) != null ? _ref3 : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); + thumbnail = canvas.toDataURL("image/png"); + _this.emit("thumbnail", file, thumbnail); + if (callback != null) { + return callback(); + } + }; + })(this); + if (callback != null) { + img.onerror = callback; + } + return img.src = imageUrl; + }; + + Dropzone.prototype.processQueue = function() { + var i, parallelUploads, processingLength, queuedFiles; + parallelUploads = this.options.parallelUploads; + processingLength = this.getUploadingFiles().length; + i = processingLength; + if (processingLength >= parallelUploads) { + return; + } + queuedFiles = this.getQueuedFiles(); + if (!(queuedFiles.length > 0)) { + return; + } + if (this.options.uploadMultiple) { + return this.processFiles(queuedFiles.slice(0, parallelUploads - processingLength)); + } else { + while (i < parallelUploads) { + if (!queuedFiles.length) { + return; + } + this.processFile(queuedFiles.shift()); + i++; + } + } + }; + + Dropzone.prototype.processFile = function(file) { + return this.processFiles([file]); + }; + + Dropzone.prototype.processFiles = function(files) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.processing = true; + file.status = Dropzone.UPLOADING; + this.emit("processing", file); + } + if (this.options.uploadMultiple) { + this.emit("processingmultiple", files); + } + return this.uploadFiles(files); + }; + + Dropzone.prototype._getFilesWithXhr = function(xhr) { + var file, files; + return files = (function() { + var _i, _len, _ref, _results; + _ref = this.files; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.xhr === xhr) { + _results.push(file); + } + } + return _results; + }).call(this); + }; + + Dropzone.prototype.cancelUpload = function(file) { + var groupedFile, groupedFiles, _i, _j, _len, _len1, _ref; + if (file.status === Dropzone.UPLOADING) { + groupedFiles = this._getFilesWithXhr(file.xhr); + for (_i = 0, _len = groupedFiles.length; _i < _len; _i++) { + groupedFile = groupedFiles[_i]; + groupedFile.status = Dropzone.CANCELED; + } + file.xhr.abort(); + for (_j = 0, _len1 = groupedFiles.length; _j < _len1; _j++) { + groupedFile = groupedFiles[_j]; + this.emit("canceled", groupedFile); + } + if (this.options.uploadMultiple) { + this.emit("canceledmultiple", groupedFiles); + } + } else if ((_ref = file.status) === Dropzone.ADDED || _ref === Dropzone.QUEUED) { + file.status = Dropzone.CANCELED; + this.emit("canceled", file); + if (this.options.uploadMultiple) { + this.emit("canceledmultiple", [file]); + } + } + if (this.options.autoProcessQueue) { + return this.processQueue(); + } + }; + + resolveOption = function() { + var args, option; + option = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + if (typeof option === 'function') { + return option.apply(this, args); + } + return option; + }; + + Dropzone.prototype.uploadFile = function(file) { + return this.uploadFiles([file]); + }; + + Dropzone.prototype.uploadFiles = function(files) { + var file, formData, handleError, headerName, headerValue, headers, i, input, inputName, inputType, key, method, option, progressObj, response, updateProgress, url, value, xhr, _i, _j, _k, _l, _len, _len1, _len2, _len3, _m, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + xhr = new XMLHttpRequest(); + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.xhr = xhr; + } + method = resolveOption(this.options.method, files); + url = resolveOption(this.options.url, files); + xhr.open(method, url, true); + xhr.withCredentials = !!this.options.withCredentials; + response = null; + handleError = (function(_this) { + return function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = files.length; _j < _len1; _j++) { + file = files[_j]; + _results.push(_this._errorProcessing(files, response || _this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr)); + } + return _results; + }; + })(this); + updateProgress = (function(_this) { + return function(e) { + var allFilesFinished, progress, _j, _k, _l, _len1, _len2, _len3, _results; + if (e != null) { + progress = 100 * e.loaded / e.total; + for (_j = 0, _len1 = files.length; _j < _len1; _j++) { + file = files[_j]; + file.upload = { + progress: progress, + total: e.total, + bytesSent: e.loaded + }; + } + } else { + allFilesFinished = true; + progress = 100; + for (_k = 0, _len2 = files.length; _k < _len2; _k++) { + file = files[_k]; + if (!(file.upload.progress === 100 && file.upload.bytesSent === file.upload.total)) { + allFilesFinished = false; + } + file.upload.progress = progress; + file.upload.bytesSent = file.upload.total; + } + if (allFilesFinished) { + return; + } + } + _results = []; + for (_l = 0, _len3 = files.length; _l < _len3; _l++) { + file = files[_l]; + _results.push(_this.emit("uploadprogress", file, progress, file.upload.bytesSent)); + } + return _results; + }; + })(this); + xhr.onload = (function(_this) { + return function(e) { + var _ref; + if (files[0].status === Dropzone.CANCELED) { + return; + } + if (xhr.readyState !== 4) { + return; + } + response = xhr.responseText; + if (xhr.getResponseHeader("content-type") && ~xhr.getResponseHeader("content-type").indexOf("application/json")) { + try { + response = JSON.parse(response); + } catch (_error) { + e = _error; + response = "Invalid JSON response from server."; + } + } + updateProgress(); + if (!((200 <= (_ref = xhr.status) && _ref < 300))) { + return handleError(); + } else { + return _this._finished(files, response, e); + } + }; + })(this); + xhr.onerror = (function(_this) { + return function() { + if (files[0].status === Dropzone.CANCELED) { + return; + } + return handleError(); + }; + })(this); + progressObj = (_ref = xhr.upload) != null ? _ref : xhr; + progressObj.onprogress = updateProgress; + headers = { + "Accept": "application/json", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest" + }; + if (this.options.headers) { + extend(headers, this.options.headers); + } + for (headerName in headers) { + headerValue = headers[headerName]; + if (headerValue) { + xhr.setRequestHeader(headerName, headerValue); + } + } + formData = new FormData(); + if (this.options.params) { + _ref1 = this.options.params; + for (key in _ref1) { + value = _ref1[key]; + formData.append(key, value); + } + } + for (_j = 0, _len1 = files.length; _j < _len1; _j++) { + file = files[_j]; + this.emit("sending", file, xhr, formData); + } + if (this.options.uploadMultiple) { + this.emit("sendingmultiple", files, xhr, formData); + } + if (this.element.tagName === "FORM") { + _ref2 = this.element.querySelectorAll("input, textarea, select, button"); + for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { + input = _ref2[_k]; + inputName = input.getAttribute("name"); + inputType = input.getAttribute("type"); + if (input.tagName === "SELECT" && input.hasAttribute("multiple")) { + _ref3 = input.options; + for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { + option = _ref3[_l]; + if (option.selected) { + formData.append(inputName, option.value); + } + } + } else if (!inputType || ((_ref4 = inputType.toLowerCase()) !== "checkbox" && _ref4 !== "radio") || input.checked) { + formData.append(inputName, input.value); + } + } + } + for (i = _m = 0, _ref5 = files.length - 1; 0 <= _ref5 ? _m <= _ref5 : _m >= _ref5; i = 0 <= _ref5 ? ++_m : --_m) { + formData.append(this._getParamName(i), files[i], this._renameFilename(files[i].name)); + } + return this.submitRequest(xhr, formData, files); + }; + + Dropzone.prototype.submitRequest = function(xhr, formData, files) { + return xhr.send(formData); + }; + + Dropzone.prototype._finished = function(files, responseText, e) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.status = Dropzone.SUCCESS; + this.emit("success", file, responseText, e); + this.emit("complete", file); + } + if (this.options.uploadMultiple) { + this.emit("successmultiple", files, responseText, e); + this.emit("completemultiple", files); + } + if (this.options.autoProcessQueue) { + return this.processQueue(); + } + }; + + Dropzone.prototype._errorProcessing = function(files, message, xhr) { + var file, _i, _len; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + file.status = Dropzone.ERROR; + this.emit("error", file, message, xhr); + this.emit("complete", file); + } + if (this.options.uploadMultiple) { + this.emit("errormultiple", files, message, xhr); + this.emit("completemultiple", files); + } + if (this.options.autoProcessQueue) { + return this.processQueue(); + } + }; + + return Dropzone; + +})(Emitter); + +Dropzone.version = "4.3.0"; + +Dropzone.options = {}; + +Dropzone.optionsForElement = function(element) { + if (element.getAttribute("id")) { + return Dropzone.options[camelize(element.getAttribute("id"))]; + } else { + return void 0; + } +}; + +Dropzone.instances = []; + +Dropzone.forElement = function(element) { + if (typeof element === "string") { + element = document.querySelector(element); + } + if ((element != null ? element.dropzone : void 0) == null) { + 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."); + } + return element.dropzone; +}; + +Dropzone.autoDiscover = true; + +Dropzone.discover = function() { + var checkElements, dropzone, dropzones, _i, _len, _results; + if (document.querySelectorAll) { + dropzones = document.querySelectorAll(".dropzone"); + } else { + dropzones = []; + checkElements = function(elements) { + var el, _i, _len, _results; + _results = []; + for (_i = 0, _len = elements.length; _i < _len; _i++) { + el = elements[_i]; + if (/(^| )dropzone($| )/.test(el.className)) { + _results.push(dropzones.push(el)); + } else { + _results.push(void 0); + } + } + return _results; + }; + checkElements(document.getElementsByTagName("div")); + checkElements(document.getElementsByTagName("form")); + } + _results = []; + for (_i = 0, _len = dropzones.length; _i < _len; _i++) { + dropzone = dropzones[_i]; + if (Dropzone.optionsForElement(dropzone) !== false) { + _results.push(new Dropzone(dropzone)); + } else { + _results.push(void 0); + } + } + return _results; +}; + +Dropzone.blacklistedBrowsers = [/opera.*Macintosh.*version\/12/i]; + +Dropzone.isBrowserSupported = function() { + var capableBrowser, regex, _i, _len, _ref; + capableBrowser = true; + if (window.File && window.FileReader && window.FileList && window.Blob && window.FormData && document.querySelector) { + if (!("classList" in document.createElement("a"))) { + capableBrowser = false; + } else { + _ref = Dropzone.blacklistedBrowsers; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + regex = _ref[_i]; + if (regex.test(navigator.userAgent)) { + capableBrowser = false; + continue; + } + } + } + } else { + capableBrowser = false; + } + return capableBrowser; +}; + +without = function(list, rejectedItem) { + var item, _i, _len, _results; + _results = []; + for (_i = 0, _len = list.length; _i < _len; _i++) { + item = list[_i]; + if (item !== rejectedItem) { + _results.push(item); + } + } + return _results; +}; + +camelize = function(str) { + return str.replace(/[\-_](\w)/g, function(match) { + return match.charAt(1).toUpperCase(); + }); +}; + +Dropzone.createElement = function(string) { + var div; + div = document.createElement("div"); + div.innerHTML = string; + return div.childNodes[0]; +}; + +Dropzone.elementInside = function(element, container) { + if (element === container) { + return true; + } + while (element = element.parentNode) { + if (element === container) { + return true; + } + } + return false; +}; + +Dropzone.getElement = function(el, name) { + var element; + if (typeof el === "string") { + element = document.querySelector(el); + } else if (el.nodeType != null) { + element = el; + } + if (element == null) { + throw new Error("Invalid `" + name + "` option provided. Please provide a CSS selector or a plain HTML element."); + } + return element; +}; + +Dropzone.getElements = function(els, name) { + var e, el, elements, _i, _j, _len, _len1, _ref; + if (els instanceof Array) { + elements = []; + try { + for (_i = 0, _len = els.length; _i < _len; _i++) { + el = els[_i]; + elements.push(this.getElement(el, name)); + } + } catch (_error) { + e = _error; + elements = null; + } + } else if (typeof els === "string") { + elements = []; + _ref = document.querySelectorAll(els); + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + el = _ref[_j]; + elements.push(el); + } + } else if (els.nodeType != null) { + elements = [els]; + } + if (!((elements != null) && elements.length)) { + throw new Error("Invalid `" + name + "` option provided. Please provide a CSS selector, a plain HTML element or a list of those."); + } + return elements; +}; + +Dropzone.confirm = function(question, accepted, rejected) { + if (window.confirm(question)) { + return accepted(); + } else if (rejected != null) { + return rejected(); + } +}; + +Dropzone.isValidFile = function(file, acceptedFiles) { + var baseMimeType, mimeType, validType, _i, _len; + if (!acceptedFiles) { + return true; + } + acceptedFiles = acceptedFiles.split(","); + mimeType = file.type; + baseMimeType = mimeType.replace(/\/.*$/, ""); + for (_i = 0, _len = acceptedFiles.length; _i < _len; _i++) { + validType = acceptedFiles[_i]; + validType = validType.trim(); + if (validType.charAt(0) === ".") { + if (file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) !== -1) { + return true; + } + } else if (/\/\*$/.test(validType)) { + if (baseMimeType === validType.replace(/\/.*$/, "")) { + return true; + } + } else { + if (mimeType === validType) { + return true; + } + } + } + return false; +}; + +if (typeof jQuery !== "undefined" && jQuery !== null) { + jQuery.fn.dropzone = function(options) { + return this.each(function() { + return new Dropzone(this, options); + }); + }; +} + +if (typeof module !== "undefined" && module !== null) { + module.exports = Dropzone; +} else { + window.Dropzone = Dropzone; +} + +Dropzone.ADDED = "added"; + +Dropzone.QUEUED = "queued"; + +Dropzone.ACCEPTED = Dropzone.QUEUED; + +Dropzone.UPLOADING = "uploading"; + +Dropzone.PROCESSING = Dropzone.UPLOADING; + +Dropzone.CANCELED = "canceled"; + +Dropzone.ERROR = "error"; + +Dropzone.SUCCESS = "success"; + + +/* + +Bugfix for iOS 6 and 7 +Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios +based on the work of https://github.com/stomita/ios-imagefile-megapixel + */ + +detectVerticalSquash = function(img) { + var alpha, canvas, ctx, data, ey, ih, iw, py, ratio, sy; + iw = img.naturalWidth; + ih = img.naturalHeight; + canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = ih; + ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + data = ctx.getImageData(0, 0, 1, ih).data; + sy = 0; + ey = ih; + py = ih; + while (py > sy) { + alpha = data[(py - 1) * 4 + 3]; + if (alpha === 0) { + ey = py; + } else { + sy = py; + } + py = (ey + sy) >> 1; + } + ratio = py / ih; + if (ratio === 0) { + return 1; + } else { + return ratio; + } +}; + +drawImageIOSFix = function(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) { + var vertSquashRatio; + vertSquashRatio = detectVerticalSquash(img); + return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); +}; + + +/* + * contentloaded.js + * + * Author: Diego Perini (diego.perini at gmail.com) + * Summary: cross-browser wrapper for DOMContentLoaded + * Updated: 20101020 + * License: MIT + * Version: 1.2 + * + * URL: + * http://javascript.nwbox.com/ContentLoaded/ + * http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE + */ + +contentLoaded = function(win, fn) { + var add, doc, done, init, poll, pre, rem, root, top; + done = false; + top = true; + doc = win.document; + root = doc.documentElement; + add = (doc.addEventListener ? "addEventListener" : "attachEvent"); + rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); + pre = (doc.addEventListener ? "" : "on"); + init = function(e) { + if (e.type === "readystatechange" && doc.readyState !== "complete") { + return; + } + (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); + if (!done && (done = true)) { + return fn.call(win, e.type || e); + } + }; + poll = function() { + var e; + try { + root.doScroll("left"); + } catch (_error) { + e = _error; + setTimeout(poll, 50); + return; + } + return init("poll"); + }; + if (doc.readyState !== "complete") { + if (doc.createEventObject && root.doScroll) { + try { + top = !win.frameElement; + } catch (_error) {} + if (top) { + poll(); + } + } + doc[add](pre + "DOMContentLoaded", init, false); + doc[add](pre + "readystatechange", init, false); + return win[add](pre + "load", init, false); + } +}; + +Dropzone._autoDiscoverFunction = function() { + if (Dropzone.autoDiscover) { + return Dropzone.discover(); + } +}; + +contentLoaded(window, Dropzone._autoDiscoverFunction); diff --git a/modules/files/site/files/js/src/basic.scss b/modules/files/site/files/js/src/basic.scss new file mode 100644 index 0000000..52b9329 --- /dev/null +++ b/modules/files/site/files/js/src/basic.scss @@ -0,0 +1,78 @@ +/* + * The MIT License + * Copyright (c) 2012 Matias Meno + */ + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +.dropzone, .dropzone * { + box-sizing: border-box; +} +.dropzone { + + position: relative; + + .dz-preview { + position: relative; + display: inline-block; + width: 120px; + margin: 0.5em; + + .dz-progress { + display: block; + height: 15px; + border: 1px solid #aaa; + .dz-upload { + display: block; + height: 100%; + width: 0; + background: green; + } + } + + .dz-error-message { + color: red; + display: none; + } + &.dz-error { + .dz-error-message, .dz-error-mark { + display: block; + } + } + &.dz-success { + .dz-success-mark { + display: block; + } + } + + .dz-error-mark, .dz-success-mark { + position: absolute; + display: none; + left: 30px; + top: 30px; + width: 54px; + height: 58px; + left: 50%; + margin-left: -(54px/2); + } + + + } + +} \ No newline at end of file diff --git a/modules/files/site/files/js/src/dropzone.coffee b/modules/files/site/files/js/src/dropzone.coffee new file mode 100644 index 0000000..d9bd223 --- /dev/null +++ b/modules/files/site/files/js/src/dropzone.coffee @@ -0,0 +1,1590 @@ +### +# +# More info at [www.dropzonejs.com](http://www.dropzonejs.com) +# +# Copyright (c) 2012, Matias Meno +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +### + + +noop = -> + + +# The Emitter class provides the ability to call `.on()` on Dropzone to listen +# to events. +# It is strongly based on component's emitter class, and I removed the +# functionality because of the dependency hell with different frameworks. +class Emitter + + # Add an event listener for given event + addEventListener: @::on + on: (event, fn) -> + @_callbacks = @_callbacks || {} + # Create namespace for this event + @_callbacks[event] = [] unless @_callbacks[event] + @_callbacks[event].push fn + return @ + + + emit: (event, args...) -> + @_callbacks = @_callbacks || {} + callbacks = @_callbacks[event] + + if callbacks + callback.apply @, args for callback in callbacks + + return @ + + # Remove event listener for given event. If fn is not provided, all event + # listeners for that event will be removed. If neither is provided, all + # event listeners will be removed. + removeListener: @::off + removeAllListeners: @::off + removeEventListener: @::off + off: (event, fn) -> + if !@_callbacks || arguments.length == 0 + @_callbacks = {} + return @ + + # specific event + callbacks = @_callbacks[event] + return @ unless callbacks + + # remove all handlers + if arguments.length == 1 + delete @_callbacks[event] + return @ + + # remove specific handler + for callback, i in callbacks + if callback == fn + callbacks.splice i, 1 + break + + return @ + +class Dropzone extends Emitter + + # Exposing the emitter class, mainly for tests + Emitter: Emitter + + ### + This is a list of all available events you can register on a dropzone object. + + You can register an event handler like this: + + dropzone.on("dragEnter", function() { }); + + ### + events: [ + "drop" + "dragstart" + "dragend" + "dragenter" + "dragover" + "dragleave" + "addedfile" + "addedfiles" + "removedfile" + "thumbnail" + "error" + "errormultiple" + "processing" + "processingmultiple" + "uploadprogress" + "totaluploadprogress" + "sending" + "sendingmultiple" + "success" + "successmultiple" + "canceled" + "canceledmultiple" + "complete" + "completemultiple" + "reset" + "maxfilesexceeded" + "maxfilesreached" + "queuecomplete" + ] + + + + defaultOptions: + url: null + method: "post" + withCredentials: no + parallelUploads: 2 + uploadMultiple: no # Whether to send multiple files in one request. + maxFilesize: 256 # in MB + paramName: "file" # The name of the file param that gets transferred. + createImageThumbnails: true + maxThumbnailFilesize: 10 # in MB. When the filename exceeds this limit, the thumbnail will not be generated. + thumbnailWidth: 120 + thumbnailHeight: 120 + + # The base that is used to calculate the filesize. You can change this to + # 1024 if you would rather display kibibytes, mebibytes, etc... + # 1024 is technically incorrect, + # because `1024 bytes` are `1 kibibyte` not `1 kilobyte`. + # You can change this to `1024` if you don't care about validity. + filesizeBase: 1000 + + # Can be used to limit the maximum number of files that will be handled + # by this Dropzone + maxFiles: null + + # Can be an object of additional parameters to transfer to the server. + # This is the same as adding hidden input fields in the form element. + params: { } + + # If true, the dropzone will present a file selector when clicked. + clickable: yes + + # Whether hidden files in directories should be ignored. + ignoreHiddenFiles: yes + + # You can set accepted mime types here. + # + # The default implementation of the `accept()` function will check this + # property, and if the Dropzone is clickable this will be used as + # `accept` attribute. + # + # This is a comma separated list of mime types or extensions. E.g.: + # + # audio/*,video/*,image/png,.pdf + # + # See https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept + # for a reference. + acceptedFiles: null + + # @deprecated + # Use acceptedFiles instead. + acceptedMimeTypes: null + + # If false, files will be added to the queue but the queue will not be + # processed automatically. + # This can be useful if you need some additional user input before sending + # files (or if you want want all files sent at once). + # If you're ready to send the file simply call myDropzone.processQueue() + autoProcessQueue: on + + # If false, files added to the dropzone will not be queued by default. + # You'll have to call `enqueueFile(file)` manually. + autoQueue: on + + # If true, Dropzone will add a link to each file preview to cancel/remove + # the upload. + # See dictCancelUpload and dictRemoveFile to use different words. + addRemoveLinks: no + + # A CSS selector or HTML element for the file previews container. + # If null, the dropzone element itself will be used. + # If false, previews won't be rendered. + previewsContainer: null + + # Selector for hidden input container + hiddenInputContainer: "body" + + # If null, no capture type will be specified + # If camera, mobile devices will skip the file selection and choose camera + # If microphone, mobile devices will skip the file selection and choose the microphone + # If camcorder, mobile devices will skip the file selection and choose the camera in video mode + # On apple devices multiple must be set to false. AcceptedFiles may need to + # be set to an appropriate mime type (e.g. "image/*", "audio/*", or "video/*"). + capture: null + + # Before the file is appended to the formData, the function _renameFilename is performed for file.name + # which executes the function defined in renameFilename + renameFilename: null + + # Dictionary + + # The text used before any files are dropped + dictDefaultMessage: "Drop files here to upload" + + # The text that replaces the default message text it the browser is not supported + dictFallbackMessage: "Your browser does not support drag'n'drop file uploads." + + # The text that will be added before the fallback form + # If null, no text will be added at all. + dictFallbackText: "Please use the fallback form below to upload your files like in the olden days." + + # If the filesize is too big. + dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB." + + # If the file doesn't match the file type. + dictInvalidFileType: "You can't upload files of this type." + + # If the server response was invalid. + dictResponseError: "Server responded with {{statusCode}} code." + + # If used, the text to be used for the cancel upload link. + dictCancelUpload: "Cancel upload" + + # If used, the text to be used for confirmation when cancelling upload. + dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?" + + # If used, the text to be used to remove a file. + dictRemoveFile: "Remove file" + + # If this is not null, then the user will be prompted before removing a file. + dictRemoveFileConfirmation: null + + # Displayed when the maxFiles have been exceeded + # You can use {{maxFiles}} here, which will be replaced by the option. + dictMaxFilesExceeded: "You can not upload any more files." + + + # If `done()` is called without argument the file is accepted + # If you call it with an error message, the file is rejected + # (This allows for asynchronous validation). + accept: (file, done) -> done() + + + # Called when dropzone initialized + # You can add event listeners here + init: -> noop + + # Used to debug dropzone and force the fallback form. + forceFallback: off + + # Called when the browser does not support drag and drop + fallback: -> + # This code should pass in IE7... :( + @element.className = "#{@element.className} dz-browser-not-supported" + + for child in @element.getElementsByTagName "div" + if /(^| )dz-message($| )/.test child.className + messageElement = child + child.className = "dz-message" # Removes the 'dz-default' class + continue + unless messageElement + messageElement = Dropzone.createElement """
""" + @element.appendChild messageElement + + span = messageElement.getElementsByTagName("span")[0] + if span + if span.textContent? + span.textContent = @options.dictFallbackMessage + else if span.innerText? + span.innerText = @options.dictFallbackMessage + + @element.appendChild @getFallbackForm() + + + + # Gets called to calculate the thumbnail dimensions. + # + # You can use file.width, file.height, options.thumbnailWidth and + # options.thumbnailHeight to calculate the dimensions. + # + # The dimensions are going to be used like this: + # + # var info = @options.resize.call(this, file); + # ctx.drawImage(img, info.srcX, info.srcY, info.srcWidth, info.srcHeight, info.trgX, info.trgY, info.trgWidth, info.trgHeight); + # + # srcX, srcy, trgX and trgY can be omitted (in which case 0 is assumed). + # trgWidth and trgHeight can be omitted (in which case the options.thumbnailWidth / options.thumbnailHeight are used) + resize: (file) -> + info = + srcX: 0 + srcY: 0 + srcWidth: file.width + srcHeight: file.height + + srcRatio = file.width / file.height + + info.optWidth = @options.thumbnailWidth + info.optHeight = @options.thumbnailHeight + + # automatically calculate dimensions if not specified + if !info.optWidth? and !info.optHeight? + info.optWidth = info.srcWidth + info.optHeight = info.srcHeight + else if !info.optWidth? + info.optWidth = srcRatio * info.optHeight + else if !info.optHeight? + info.optHeight = (1/srcRatio) * info.optWidth + + trgRatio = info.optWidth / info.optHeight + + if file.height < info.optHeight or file.width < info.optWidth + # This image is smaller than the canvas + info.trgHeight = info.srcHeight + info.trgWidth = info.srcWidth + + else + # Image is bigger and needs rescaling + if srcRatio > trgRatio + info.srcHeight = file.height + info.srcWidth = info.srcHeight * trgRatio + else + info.srcWidth = file.width + info.srcHeight = info.srcWidth / trgRatio + + info.srcX = (file.width - info.srcWidth) / 2 + info.srcY = (file.height - info.srcHeight) / 2 + + return info + + + ### + Those functions register themselves to the events on init and handle all + the user interface specific stuff. Overwriting them won't break the upload + but can break the way it's displayed. + You can overwrite them if you don't like the default behavior. If you just + want to add an additional event handler, register it on the dropzone object + and don't overwrite those options. + ### + + + + + # Those are self explanatory and simply concern the DragnDrop. + drop: (e) -> @element.classList.remove "dz-drag-hover" + dragstart: noop + dragend: (e) -> @element.classList.remove "dz-drag-hover" + dragenter: (e) -> @element.classList.add "dz-drag-hover" + dragover: (e) -> @element.classList.add "dz-drag-hover" + dragleave: (e) -> @element.classList.remove "dz-drag-hover" + + paste: noop + + # Called whenever there are no files left in the dropzone anymore, and the + # dropzone should be displayed as if in the initial state. + reset: -> + @element.classList.remove "dz-started" + + # Called when a file is added to the queue + # Receives `file` + addedfile: (file) -> + @element.classList.add "dz-started" if @element == @previewsContainer + + if @previewsContainer + file.previewElement = Dropzone.createElement @options.previewTemplate.trim() + file.previewTemplate = file.previewElement # Backwards compatibility + + @previewsContainer.appendChild file.previewElement + node.textContent = @_renameFilename(file.name) for node in file.previewElement.querySelectorAll("[data-dz-name]") + node.innerHTML = @filesize file.size for node in file.previewElement.querySelectorAll("[data-dz-size]") + + if @options.addRemoveLinks + file._removeLink = Dropzone.createElement """#{@options.dictRemoveFile}""" + file.previewElement.appendChild file._removeLink + + removeFileEvent = (e) => + e.preventDefault() + e.stopPropagation() + if file.status == Dropzone.UPLOADING + Dropzone.confirm @options.dictCancelUploadConfirmation, => @removeFile file + else + if @options.dictRemoveFileConfirmation + Dropzone.confirm @options.dictRemoveFileConfirmation, => @removeFile file + else + @removeFile file + + removeLink.addEventListener "click", removeFileEvent for removeLink in file.previewElement.querySelectorAll("[data-dz-remove]") + + + # Called whenever a file is removed. + removedfile: (file) -> + file.previewElement?.parentNode.removeChild file.previewElement if file.previewElement + @_updateMaxFilesReachedClass() + + # Called when a thumbnail has been generated + # Receives `file` and `dataUrl` + thumbnail: (file, dataUrl) -> + if file.previewElement + file.previewElement.classList.remove "dz-file-preview" + for thumbnailElement in file.previewElement.querySelectorAll("[data-dz-thumbnail]") + thumbnailElement.alt = file.name + thumbnailElement.src = dataUrl + + setTimeout (=> file.previewElement.classList.add "dz-image-preview"), 1 + + # Called whenever an error occurs + # Receives `file` and `message` + error: (file, message) -> + if file.previewElement + file.previewElement.classList.add "dz-error" + message = message.error if typeof message != "String" and message.error + node.textContent = message for node in file.previewElement.querySelectorAll("[data-dz-errormessage]") + + errormultiple: noop + + # Called when a file gets processed. Since there is a cue, not all added + # files are processed immediately. + # Receives `file` + processing: (file) -> + if file.previewElement + file.previewElement.classList.add "dz-processing" + file._removeLink.textContent = @options.dictCancelUpload if file._removeLink + + processingmultiple: noop + + # Called whenever the upload progress gets updated. + # Receives `file`, `progress` (percentage 0-100) and `bytesSent`. + # To get the total number of bytes of the file, use `file.size` + uploadprogress: (file, progress, bytesSent) -> + if file.previewElement + for node in file.previewElement.querySelectorAll("[data-dz-uploadprogress]") + if node.nodeName is 'PROGRESS' + node.value = progress + else + node.style.width = "#{progress}%" + + # Called whenever the total upload progress gets updated. + # Called with totalUploadProgress (0-100), totalBytes and totalBytesSent + totaluploadprogress: noop + + # Called just before the file is sent. Gets the `xhr` object as second + # parameter, so you can modify it (for example to add a CSRF token) and a + # `formData` object to add additional information. + sending: noop + + sendingmultiple: noop + + # When the complete upload is finished and successful + # Receives `file` + success: (file) -> + file.previewElement.classList.add "dz-success" if file.previewElement + + successmultiple: noop + + # When the upload is canceled. + canceled: (file) -> @emit "error", file, "Upload canceled." + + canceledmultiple: noop + + # When the upload is finished, either with success or an error. + # Receives `file` + complete: (file) -> + file._removeLink.textContent = @options.dictRemoveFile if file._removeLink + file.previewElement.classList.add "dz-complete" if file.previewElement + + completemultiple: noop + + maxfilesexceeded: noop + + maxfilesreached: noop + + queuecomplete: noop + + addedfiles: noop + + + # This template will be chosen when a new file is dropped. + previewTemplate: """ +
+
+
+
+
+
+
+
+
+ + Check + + + + + +
+
+ + Error + + + + + + + +
+
+ """ + + # global utility + extend = (target, objects...) -> + for object in objects + target[key] = val for key, val of object + target + + constructor: (@element, options) -> + # For backwards compatibility since the version was in the prototype previously + @version = Dropzone.version + + @defaultOptions.previewTemplate = @defaultOptions.previewTemplate.replace /\n*/g, "" + + @clickableElements = [ ] + @listeners = [ ] + @files = [] # All files + + @element = document.querySelector @element if typeof @element == "string" + + # Not checking if instance of HTMLElement or Element since IE9 is extremely weird. + throw new Error "Invalid dropzone element." unless @element and @element.nodeType? + + throw new Error "Dropzone already attached." if @element.dropzone + + # Now add this dropzone to the instances. + Dropzone.instances.push @ + + # Put the dropzone inside the element itself. + @element.dropzone = @ + + elementOptions = Dropzone.optionsForElement(@element) ? { } + + @options = extend { }, @defaultOptions, elementOptions, options ? { } + + # If the browser failed, just call the fallback and leave + return @options.fallback.call this if @options.forceFallback or !Dropzone.isBrowserSupported() + + # @options.url = @element.getAttribute "action" unless @options.url? + @options.url = @element.getAttribute "action" unless @options.url? + + throw new Error "No URL provided." unless @options.url + + throw new Error "You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated." if @options.acceptedFiles and @options.acceptedMimeTypes + + # Backwards compatibility + if @options.acceptedMimeTypes + @options.acceptedFiles = @options.acceptedMimeTypes + delete @options.acceptedMimeTypes + + @options.method = @options.method.toUpperCase() + + if (fallback = @getExistingFallback()) and fallback.parentNode + # Remove the fallback + fallback.parentNode.removeChild fallback + + # Display previews in the previewsContainer element or the Dropzone element unless explicitly set to false + if @options.previewsContainer != false + if @options.previewsContainer + @previewsContainer = Dropzone.getElement @options.previewsContainer, "previewsContainer" + else + @previewsContainer = @element + + if @options.clickable + if @options.clickable == yes + @clickableElements = [ @element ] + else + @clickableElements = Dropzone.getElements @options.clickable, "clickable" + + + @init() + + + # Returns all files that have been accepted + getAcceptedFiles: -> file for file in @files when file.accepted + + # Returns all files that have been rejected + # Not sure when that's going to be useful, but added for completeness. + getRejectedFiles: -> file for file in @files when not file.accepted + + getFilesWithStatus: (status) -> file for file in @files when file.status == status + + # Returns all files that are in the queue + getQueuedFiles: -> @getFilesWithStatus Dropzone.QUEUED + + getUploadingFiles: -> @getFilesWithStatus Dropzone.UPLOADING + + getAddedFiles: -> @getFilesWithStatus Dropzone.ADDED + + # Files that are either queued or uploading + getActiveFiles: -> file for file in @files when file.status == Dropzone.UPLOADING or file.status == Dropzone.QUEUED + + + init: -> + # In case it isn't set already + @element.setAttribute("enctype", "multipart/form-data") if @element.tagName == "form" + + if @element.classList.contains("dropzone") and !@element.querySelector(".dz-message") + @element.appendChild Dropzone.createElement """
#{@options.dictDefaultMessage}
""" + + if @clickableElements.length + setupHiddenFileInput = => + @hiddenFileInput.parentNode.removeChild @hiddenFileInput if @hiddenFileInput + @hiddenFileInput = document.createElement "input" + @hiddenFileInput.setAttribute "type", "file" + @hiddenFileInput.setAttribute "multiple", "multiple" if !@options.maxFiles? || @options.maxFiles > 1 + @hiddenFileInput.className = "dz-hidden-input" + + @hiddenFileInput.setAttribute "accept", @options.acceptedFiles if @options.acceptedFiles? + @hiddenFileInput.setAttribute "capture", @options.capture if @options.capture? + + # Not setting `display="none"` because some browsers don't accept clicks + # on elements that aren't displayed. + @hiddenFileInput.style.visibility = "hidden" + @hiddenFileInput.style.position = "absolute" + @hiddenFileInput.style.top = "0" + @hiddenFileInput.style.left = "0" + @hiddenFileInput.style.height = "0" + @hiddenFileInput.style.width = "0" + document.querySelector(@options.hiddenInputContainer).appendChild @hiddenFileInput + @hiddenFileInput.addEventListener "change", => + files = @hiddenFileInput.files + @addFile file for file in files if files.length + @emit "addedfiles", files + setupHiddenFileInput() + setupHiddenFileInput() + + @URL = window.URL ? window.webkitURL + + + # Setup all event listeners on the Dropzone object itself. + # They're not in @setupEventListeners() because they shouldn't be removed + # again when the dropzone gets disabled. + @on eventName, @options[eventName] for eventName in @events + + @on "uploadprogress", => @updateTotalUploadProgress() + + @on "removedfile", => @updateTotalUploadProgress() + + @on "canceled", (file) => @emit "complete", file + + # Emit a `queuecomplete` event if all files finished uploading. + @on "complete", (file) => + if @getAddedFiles().length == 0 and @getUploadingFiles().length == 0 and @getQueuedFiles().length == 0 + # This needs to be deferred so that `queuecomplete` really triggers after `complete` + setTimeout (=> @emit "queuecomplete"), 0 + + + noPropagation = (e) -> + e.stopPropagation() + if e.preventDefault + e.preventDefault() + else + e.returnValue = false + + # Create the listeners + @listeners = [ + { + element: @element + events: + "dragstart": (e) => + @emit "dragstart", e + "dragenter": (e) => + noPropagation e + @emit "dragenter", e + "dragover": (e) => + # Makes it possible to drag files from chrome's download bar + # http://stackoverflow.com/questions/19526430/drag-and-drop-file-uploads-from-chrome-downloads-bar + # Try is required to prevent bug in Internet Explorer 11 (SCRIPT65535 exception) + try efct = e.dataTransfer.effectAllowed + e.dataTransfer.dropEffect = if 'move' == efct or 'linkMove' == efct then 'move' else 'copy' + + noPropagation e + @emit "dragover", e + "dragleave": (e) => + @emit "dragleave", e + "drop": (e) => + noPropagation e + @drop e + "dragend": (e) => + @emit "dragend", e + + # This is disabled right now, because the browsers don't implement it properly. + # "paste": (e) => + # noPropagation e + # @paste e + } + ] + + @clickableElements.forEach (clickableElement) => + @listeners.push + element: clickableElement + events: + "click": (evt) => + # Only the actual dropzone or the message element should trigger file selection + if (clickableElement != @element) or (evt.target == @element or Dropzone.elementInside evt.target, @element.querySelector ".dz-message") + @hiddenFileInput.click() # Forward the click + return true + + @enable() + + @options.init.call @ + + # Not fully tested yet + destroy: -> + @disable() + @removeAllFiles true + if @hiddenFileInput?.parentNode + @hiddenFileInput.parentNode.removeChild @hiddenFileInput + @hiddenFileInput = null + delete @element.dropzone + Dropzone.instances.splice Dropzone.instances.indexOf(this), 1 + + + updateTotalUploadProgress: -> + totalBytesSent = 0 + totalBytes = 0 + + activeFiles = @getActiveFiles() + + if activeFiles.length + for file in @getActiveFiles() + totalBytesSent += file.upload.bytesSent + totalBytes += file.upload.total + totalUploadProgress = 100 * totalBytesSent / totalBytes + else + totalUploadProgress = 100 + + @emit "totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent + + # @options.paramName can be a function taking one parameter rather than a string. + # A parameter name for a file is obtained simply by calling this with an index number. + _getParamName: (n) -> + if typeof @options.paramName is "function" + @options.paramName n + else + "#{@options.paramName}#{if @options.uploadMultiple then "[#{n}]" else ""}" + + # If @options.renameFilename is a function, + # the function will be used to rename the file.name before appending it to the formData + _renameFilename: (name) -> + return name unless typeof @options.renameFilename is "function" + @options.renameFilename name + + # Returns a form that can be used as fallback if the browser does not support DragnDrop + # + # If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided. + # This code has to pass in IE7 :( + getFallbackForm: -> + return existingFallback if existingFallback = @getExistingFallback() + + fieldsString = """
""" + fieldsString += """

#{@options.dictFallbackText}

""" if @options.dictFallbackText + fieldsString += """
""" + + fields = Dropzone.createElement fieldsString + if @element.tagName isnt "FORM" + form = Dropzone.createElement("""
""") + form.appendChild fields + else + # Make sure that the enctype and method attributes are set properly + @element.setAttribute "enctype", "multipart/form-data" + @element.setAttribute "method", @options.method + form ? fields + + + # Returns the fallback elements if they exist already + # + # This code has to pass in IE7 :( + getExistingFallback: -> + getFallback = (elements) -> return el for el in elements when /(^| )fallback($| )/.test el.className + + for tagName in [ "div", "form" ] + return fallback if fallback = getFallback @element.getElementsByTagName tagName + + + # Activates all listeners stored in @listeners + setupEventListeners: -> + for elementListeners in @listeners + elementListeners.element.addEventListener event, listener, false for event, listener of elementListeners.events + + + # Deactivates all listeners stored in @listeners + removeEventListeners: -> + for elementListeners in @listeners + elementListeners.element.removeEventListener event, listener, false for event, listener of elementListeners.events + + # Removes all event listeners and cancels all files in the queue or being processed. + disable: -> + @clickableElements.forEach (element) -> element.classList.remove "dz-clickable" + @removeEventListeners() + + @cancelUpload file for file in @files + + enable: -> + @clickableElements.forEach (element) -> element.classList.add "dz-clickable" + @setupEventListeners() + + # Returns a nicely formatted filesize + filesize: (size) -> + selectedSize = 0 + selectedUnit = "b" + + if size > 0 + units = [ 'TB', 'GB', 'MB', 'KB', 'b' ] + + for unit, i in units + cutoff = Math.pow(@options.filesizeBase, 4 - i) / 10 + + if size >= cutoff + selectedSize = size / Math.pow(@options.filesizeBase, 4 - i) + selectedUnit = unit + break + + selectedSize = Math.round(10 * selectedSize) / 10 # Cutting of digits + + "#{selectedSize} #{selectedUnit}" + + + # Adds or removes the `dz-max-files-reached` class from the form. + _updateMaxFilesReachedClass: -> + if @options.maxFiles? and @getAcceptedFiles().length >= @options.maxFiles + @emit 'maxfilesreached', @files if @getAcceptedFiles().length == @options.maxFiles + @element.classList.add "dz-max-files-reached" + else + @element.classList.remove "dz-max-files-reached" + + + + drop: (e) -> + return unless e.dataTransfer + @emit "drop", e + + files = e.dataTransfer.files + @emit "addedfiles", files + + # Even if it's a folder, files.length will contain the folders. + if files.length + items = e.dataTransfer.items + if items and items.length and (items[0].webkitGetAsEntry?) + # The browser supports dropping of folders, so handle items instead of files + @_addFilesFromItems items + else + @handleFiles files + return + + paste: (e) -> + return unless e?.clipboardData?.items? + + @emit "paste", e + items = e.clipboardData.items + + @_addFilesFromItems items if items.length + + + handleFiles: (files) -> + @addFile file for file in files + + # When a folder is dropped (or files are pasted), items must be handled + # instead of files. + _addFilesFromItems: (items) -> + for item in items + if item.webkitGetAsEntry? and entry = item.webkitGetAsEntry() + if entry.isFile + @addFile item.getAsFile() + else if entry.isDirectory + # Append all files from that directory to files + @_addFilesFromDirectory entry, entry.name + else if item.getAsFile? + if !item.kind? or item.kind == "file" + @addFile item.getAsFile() + + + # Goes through the directory, and adds each file it finds recursively + _addFilesFromDirectory: (directory, path) -> + dirReader = directory.createReader() + + errorHandler = (error) -> console?.log? error + + readEntries = () => + dirReader.readEntries (entries) => + if entries.length > 0 + for entry in entries + if entry.isFile + entry.file (file) => + return if @options.ignoreHiddenFiles and file.name.substring(0, 1) is '.' + file.fullPath = "#{path}/#{file.name}" + @addFile file + else if entry.isDirectory + @_addFilesFromDirectory entry, "#{path}/#{entry.name}" + + # Recursively call readEntries() again, since browser only handle + # the first 100 entries. + # See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries + readEntries() + return null + , errorHandler + + readEntries() + + + + # If `done()` is called without argument the file is accepted + # If you call it with an error message, the file is rejected + # (This allows for asynchronous validation) + # + # This function checks the filesize, and if the file.type passes the + # `acceptedFiles` check. + accept: (file, done) -> + if file.size > @options.maxFilesize * 1024 * 1024 + done @options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", @options.maxFilesize) + else unless Dropzone.isValidFile file, @options.acceptedFiles + done @options.dictInvalidFileType + else if @options.maxFiles? and @getAcceptedFiles().length >= @options.maxFiles + done @options.dictMaxFilesExceeded.replace "{{maxFiles}}", @options.maxFiles + @emit "maxfilesexceeded", file + else + @options.accept.call this, file, done + + addFile: (file) -> + file.upload = + progress: 0 + # Setting the total upload size to file.size for the beginning + # It's actual different than the size to be transmitted. + total: file.size + bytesSent: 0 + @files.push file + + file.status = Dropzone.ADDED + + @emit "addedfile", file + + @_enqueueThumbnail file + + @accept file, (error) => + if error + file.accepted = false + @_errorProcessing [ file ], error # Will set the file.status + else + file.accepted = true + @enqueueFile file if @options.autoQueue # Will set .accepted = true + @_updateMaxFilesReachedClass() + + + # Wrapper for enqueueFile + enqueueFiles: (files) -> @enqueueFile file for file in files; null + + enqueueFile: (file) -> + if file.status == Dropzone.ADDED and file.accepted == true + file.status = Dropzone.QUEUED + if @options.autoProcessQueue + setTimeout (=> @processQueue()), 0 # Deferring the call + else + throw new Error "This file can't be queued because it has already been processed or was rejected." + + + _thumbnailQueue: [ ] + _processingThumbnail: no + _enqueueThumbnail: (file) -> + if @options.createImageThumbnails and file.type.match(/image.*/) and file.size <= @options.maxThumbnailFilesize * 1024 * 1024 + @_thumbnailQueue.push(file) + setTimeout (=> @_processThumbnailQueue()), 0 # Deferring the call + + _processThumbnailQueue: -> + return if @_processingThumbnail or @_thumbnailQueue.length == 0 + + @_processingThumbnail = yes + @createThumbnail @_thumbnailQueue.shift(), => + @_processingThumbnail = no + @_processThumbnailQueue() + + + # Can be called by the user to remove a file + removeFile: (file) -> + @cancelUpload file if file.status == Dropzone.UPLOADING + @files = without @files, file + + @emit "removedfile", file + @emit "reset" if @files.length == 0 + + # Removes all files that aren't currently processed from the list + removeAllFiles: (cancelIfNecessary = off) -> + # Create a copy of files since removeFile() changes the @files array. + for file in @files.slice() + @removeFile file if file.status != Dropzone.UPLOADING || cancelIfNecessary + return null + + createThumbnail: (file, callback) -> + + fileReader = new FileReader + + fileReader.onload = => + + # Don't bother creating a thumbnail for SVG images since they're vector + if file.type == "image/svg+xml" + @emit "thumbnail", file, fileReader.result + callback() if callback? + return + + @createThumbnailFromUrl file, fileReader.result, callback + + fileReader.readAsDataURL file + + createThumbnailFromUrl: (file, imageUrl, callback, crossOrigin) -> + # Not using `new Image` here because of a bug in latest Chrome versions. + # See https://github.com/enyo/dropzone/pull/226 + img = document.createElement "img" + + img.crossOrigin = crossOrigin if crossOrigin + + img.onload = => + file.width = img.width + file.height = img.height + + resizeInfo = @options.resize.call @, file + + resizeInfo.trgWidth ?= resizeInfo.optWidth + resizeInfo.trgHeight ?= resizeInfo.optHeight + + canvas = document.createElement "canvas" + ctx = canvas.getContext "2d" + canvas.width = resizeInfo.trgWidth + canvas.height = resizeInfo.trgHeight + + # This is a bugfix for iOS' scaling bug. + drawImageIOSFix ctx, img, resizeInfo.srcX ? 0, resizeInfo.srcY ? 0, resizeInfo.srcWidth, resizeInfo.srcHeight, resizeInfo.trgX ? 0, resizeInfo.trgY ? 0, resizeInfo.trgWidth, resizeInfo.trgHeight + + thumbnail = canvas.toDataURL "image/png" + + @emit "thumbnail", file, thumbnail + callback() if callback? + + img.onerror = callback if callback? + + img.src = imageUrl + + + # Goes through the queue and processes files if there aren't too many already. + processQueue: -> + parallelUploads = @options.parallelUploads + processingLength = @getUploadingFiles().length + i = processingLength + + # There are already at least as many files uploading than should be + return if processingLength >= parallelUploads + + queuedFiles = @getQueuedFiles() + + return unless queuedFiles.length > 0 + + if @options.uploadMultiple + # The files should be uploaded in one request + @processFiles queuedFiles.slice 0, (parallelUploads - processingLength) + else + while i < parallelUploads + return unless queuedFiles.length # Nothing left to process + @processFile queuedFiles.shift() + i++ + + + # Wrapper for `processFiles` + processFile: (file) -> @processFiles [ file ] + + + # Loads the file, then calls finishedLoading() + processFiles: (files) -> + for file in files + file.processing = yes # Backwards compatibility + file.status = Dropzone.UPLOADING + + @emit "processing", file + + @emit "processingmultiple", files if @options.uploadMultiple + + @uploadFiles files + + + + _getFilesWithXhr: (xhr) -> files = (file for file in @files when file.xhr == xhr) + + + # Cancels the file upload and sets the status to CANCELED + # **if** the file is actually being uploaded. + # If it's still in the queue, the file is being removed from it and the status + # set to CANCELED. + cancelUpload: (file) -> + if file.status == Dropzone.UPLOADING + groupedFiles = @_getFilesWithXhr file.xhr + groupedFile.status = Dropzone.CANCELED for groupedFile in groupedFiles + file.xhr.abort() + @emit "canceled", groupedFile for groupedFile in groupedFiles + @emit "canceledmultiple", groupedFiles if @options.uploadMultiple + + else if file.status in [ Dropzone.ADDED, Dropzone.QUEUED ] + file.status = Dropzone.CANCELED + @emit "canceled", file + @emit "canceledmultiple", [ file ] if @options.uploadMultiple + + @processQueue() if @options.autoProcessQueue + + resolveOption = (option, args...) -> + if typeof option == 'function' + return option.apply(@, args) + option + + # Wrapper for uploadFiles() + uploadFile: (file) -> @uploadFiles [ file ] + + uploadFiles: (files) -> + xhr = new XMLHttpRequest() + + # Put the xhr object in the file objects to be able to reference it later. + file.xhr = xhr for file in files + + method = resolveOption @options.method, files + url = resolveOption @options.url, files + xhr.open method, url, true + + # Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179 + xhr.withCredentials = !!@options.withCredentials + + + response = null + + handleError = => + for file in files + @_errorProcessing files, response || @options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr + + + updateProgress = (e) => + if e? + progress = 100 * e.loaded / e.total + + for file in files + file.upload = + progress: progress + total: e.total + bytesSent: e.loaded + else + # Called when the file finished uploading + + allFilesFinished = yes + + progress = 100 + + for file in files + allFilesFinished = no unless file.upload.progress == 100 and file.upload.bytesSent == file.upload.total + file.upload.progress = progress + file.upload.bytesSent = file.upload.total + + # Nothing to do, all files already at 100% + return if allFilesFinished + + for file in files + @emit "uploadprogress", file, progress, file.upload.bytesSent + + xhr.onload = (e) => + return if files[0].status == Dropzone.CANCELED + + return unless xhr.readyState is 4 + + response = xhr.responseText + + if xhr.getResponseHeader("content-type") and ~xhr.getResponseHeader("content-type").indexOf "application/json" + try + response = JSON.parse response + catch e + response = "Invalid JSON response from server." + + updateProgress() + + unless 200 <= xhr.status < 300 + handleError() + else + @_finished files, response, e + + xhr.onerror = => + return if files[0].status == Dropzone.CANCELED + handleError() + + # Some browsers do not have the .upload property + progressObj = xhr.upload ? xhr + progressObj.onprogress = updateProgress + + headers = + "Accept": "application/json", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest", + + extend headers, @options.headers if @options.headers + + for headerName, headerValue of headers + xhr.setRequestHeader headerName, headerValue if headerValue + + formData = new FormData() + + # Adding all @options parameters + formData.append key, value for key, value of @options.params if @options.params + + # Let the user add additional data if necessary + @emit "sending", file, xhr, formData for file in files + @emit "sendingmultiple", files, xhr, formData if @options.uploadMultiple + + + # Take care of other input elements + if @element.tagName == "FORM" + for input in @element.querySelectorAll "input, textarea, select, button" + inputName = input.getAttribute "name" + inputType = input.getAttribute "type" + + if input.tagName == "SELECT" and input.hasAttribute "multiple" + # Possibly multiple values + formData.append inputName, option.value for option in input.options when option.selected + else if !inputType or (inputType.toLowerCase() not in [ "checkbox", "radio" ]) or input.checked + formData.append inputName, input.value + + + # Finally add the file + # Has to be last because some servers (eg: S3) expect the file to be the + # last parameter + formData.append @_getParamName(i), files[i], @_renameFilename(files[i].name) for i in [0..files.length-1] + + @submitRequest xhr, formData, files + + submitRequest: (xhr, formData, files) -> + xhr.send formData + + # Called internally when processing is finished. + # Individual callbacks have to be called in the appropriate sections. + _finished: (files, responseText, e) -> + for file in files + file.status = Dropzone.SUCCESS + @emit "success", file, responseText, e + @emit "complete", file + if @options.uploadMultiple + @emit "successmultiple", files, responseText, e + @emit "completemultiple", files + + @processQueue() if @options.autoProcessQueue + + # Called internally when processing is finished. + # Individual callbacks have to be called in the appropriate sections. + _errorProcessing: (files, message, xhr) -> + for file in files + file.status = Dropzone.ERROR + @emit "error", file, message, xhr + @emit "complete", file + if @options.uploadMultiple + @emit "errormultiple", files, message, xhr + @emit "completemultiple", files + + @processQueue() if @options.autoProcessQueue + + + +Dropzone.version = "4.3.0" + + +# This is a map of options for your different dropzones. Add configurations +# to this object for your different dropzone elemens. +# +# Example: +# +# Dropzone.options.myDropzoneElementId = { maxFilesize: 1 }; +# +# To disable autoDiscover for a specific element, you can set `false` as an option: +# +# Dropzone.options.myDisabledElementId = false; +# +# And in html: +# +#
+Dropzone.options = { } + + +# Returns the options for an element or undefined if none available. +Dropzone.optionsForElement = (element) -> + # Get the `Dropzone.options.elementId` for this element if it exists + if element.getAttribute("id") then Dropzone.options[camelize element.getAttribute "id"] else undefined + + +# Holds a list of all dropzone instances +Dropzone.instances = [ ] + +# Returns the dropzone for given element if any +Dropzone.forElement = (element) -> + element = document.querySelector element if typeof element == "string" + 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? + return element.dropzone + + +# Set to false if you don't want Dropzone to automatically find and attach to .dropzone elements. +Dropzone.autoDiscover = on + +# Looks for all .dropzone elements and creates a dropzone for them +Dropzone.discover = -> + if document.querySelectorAll + dropzones = document.querySelectorAll ".dropzone" + else + dropzones = [ ] + # IE :( + checkElements = (elements) -> + for el in elements + dropzones.push el if /(^| )dropzone($| )/.test el.className + checkElements document.getElementsByTagName "div" + checkElements document.getElementsByTagName "form" + + for dropzone in dropzones + # Create a dropzone unless auto discover has been disabled for specific element + new Dropzone dropzone unless Dropzone.optionsForElement(dropzone) == false + + + +# Since the whole Drag'n'Drop API is pretty new, some browsers implement it, +# but not correctly. +# So I created a blacklist of userAgents. Yes, yes. Browser sniffing, I know. +# But what to do when browsers *theoretically* support an API, but crash +# when using it. +# +# This is a list of regular expressions tested against navigator.userAgent +# +# ** It should only be used on browser that *do* support the API, but +# incorrectly ** +# +Dropzone.blacklistedBrowsers = [ + # The mac os version of opera 12 seems to have a problem with the File drag'n'drop API. + /opera.*Macintosh.*version\/12/i + # /MSIE\ 10/i +] + + +# Checks if the browser is supported +Dropzone.isBrowserSupported = -> + capableBrowser = yes + + if window.File and window.FileReader and window.FileList and window.Blob and window.FormData and document.querySelector + unless "classList" of document.createElement "a" + capableBrowser = no + else + # The browser supports the API, but may be blacklisted. + for regex in Dropzone.blacklistedBrowsers + if regex.test navigator.userAgent + capableBrowser = no + continue + else + capableBrowser = no + + capableBrowser + + + + +# Returns an array without the rejected item +without = (list, rejectedItem) -> item for item in list when item isnt rejectedItem + +# abc-def_ghi -> abcDefGhi +camelize = (str) -> str.replace /[\-_](\w)/g, (match) -> match.charAt(1).toUpperCase() + +# Creates an element from string +Dropzone.createElement = (string) -> + div = document.createElement "div" + div.innerHTML = string + div.childNodes[0] + +# Tests if given element is inside (or simply is) the container +Dropzone.elementInside = (element, container) -> + return yes if element == container # Coffeescript doesn't support do/while loops + return yes while element = element.parentNode when element == container + return no + + + +Dropzone.getElement = (el, name) -> + if typeof el == "string" + element = document.querySelector el + else if el.nodeType? + element = el + throw new Error "Invalid `#{name}` option provided. Please provide a CSS selector or a plain HTML element." unless element? + return element + + +Dropzone.getElements = (els, name) -> + if els instanceof Array + elements = [ ] + try + elements.push @getElement el, name for el in els + catch e + elements = null + else if typeof els == "string" + elements = [ ] + elements.push el for el in document.querySelectorAll els + else if els.nodeType? + elements = [ els ] + + 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 + + return elements + +# Asks the user the question and calls accepted or rejected accordingly +# +# The default implementation just uses `window.confirm` and then calls the +# appropriate callback. +Dropzone.confirm = (question, accepted, rejected) -> + if window.confirm question + accepted() + else if rejected? + rejected() + +# Validates the mime type like this: +# +# https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept +Dropzone.isValidFile = (file, acceptedFiles) -> + return yes unless acceptedFiles # If there are no accepted mime types, it's OK + acceptedFiles = acceptedFiles.split "," + + mimeType = file.type + baseMimeType = mimeType.replace /\/.*$/, "" + + for validType in acceptedFiles + validType = validType.trim() + if validType.charAt(0) == "." + return yes if file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) != -1 + else if /\/\*$/.test validType + # This is something like a image/* mime type + return yes if baseMimeType == validType.replace /\/.*$/, "" + else + return yes if mimeType == validType + + return no + + +# Augment jQuery +if jQuery? + jQuery.fn.dropzone = (options) -> + this.each -> new Dropzone this, options + + + + +if module? + module.exports = Dropzone +else + window.Dropzone = Dropzone + + + + + +# Dropzone file status codes +Dropzone.ADDED = "added" + +Dropzone.QUEUED = "queued" +# For backwards compatibility. Now, if a file is accepted, it's either queued +# or uploading. +Dropzone.ACCEPTED = Dropzone.QUEUED + +Dropzone.UPLOADING = "uploading" +Dropzone.PROCESSING = Dropzone.UPLOADING # alias + +Dropzone.CANCELED = "canceled" +Dropzone.ERROR = "error" +Dropzone.SUCCESS = "success" + + + + + +### + +Bugfix for iOS 6 and 7 +Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios +based on the work of https://github.com/stomita/ios-imagefile-megapixel + +### + +# Detecting vertical squash in loaded image. +# Fixes a bug which squash image vertically while drawing into canvas for some images. +# This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel +detectVerticalSquash = (img) -> + iw = img.naturalWidth + ih = img.naturalHeight + canvas = document.createElement("canvas") + canvas.width = 1 + canvas.height = ih + ctx = canvas.getContext("2d") + ctx.drawImage img, 0, 0 + data = ctx.getImageData(0, 0, 1, ih).data + + + # search image edge pixel position in case it is squashed vertically. + sy = 0 + ey = ih + py = ih + while py > sy + alpha = data[(py - 1) * 4 + 3] + + if alpha is 0 then ey = py else sy = py + + py = (ey + sy) >> 1 + ratio = (py / ih) + + if (ratio is 0) then 1 else ratio + +# A replacement for context.drawImage +# (args are for source and destination). +drawImageIOSFix = (ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) -> + vertSquashRatio = detectVerticalSquash img + ctx.drawImage img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio + + + + + + + + +### +# contentloaded.js +# +# Author: Diego Perini (diego.perini at gmail.com) +# Summary: cross-browser wrapper for DOMContentLoaded +# Updated: 20101020 +# License: MIT +# Version: 1.2 +# +# URL: +# http://javascript.nwbox.com/ContentLoaded/ +# http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE +### + +# @win window reference +# @fn function reference +contentLoaded = (win, fn) -> + done = false + top = true + doc = win.document + root = doc.documentElement + add = (if doc.addEventListener then "addEventListener" else "attachEvent") + rem = (if doc.addEventListener then "removeEventListener" else "detachEvent") + pre = (if doc.addEventListener then "" else "on") + init = (e) -> + return if e.type is "readystatechange" and doc.readyState isnt "complete" + ((if e.type is "load" then win else doc))[rem] pre + e.type, init, false + fn.call win, e.type or e if not done and (done = true) + + poll = -> + try + root.doScroll "left" + catch e + setTimeout poll, 50 + return + init "poll" + + unless doc.readyState is "complete" + if doc.createEventObject and root.doScroll + try + top = not win.frameElement + poll() if top + doc[add] pre + "DOMContentLoaded", init, false + doc[add] pre + "readystatechange", init, false + win[add] pre + "load", init, false + + +# As a single function to be able to write tests. +Dropzone._autoDiscoverFunction = -> Dropzone.discover() if Dropzone.autoDiscover +contentLoaded window, Dropzone._autoDiscoverFunction diff --git a/modules/files/site/files/js/src/dropzone.scss b/modules/files/site/files/js/src/dropzone.scss new file mode 100644 index 0000000..b99bede --- /dev/null +++ b/modules/files/site/files/js/src/dropzone.scss @@ -0,0 +1,413 @@ +/* + * The MIT License + * Copyright (c) 2012 Matias Meno + */ + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +@mixin keyframes($name) { + @-webkit-keyframes #{$name} { + @content; + } + @-moz-keyframes #{$name} { + @content; + } + @keyframes #{$name} { + @content; + } +} + + +@mixin prefix($map, $vendors: webkit moz ms o) { + @each $prop, $value in $map { + @if $vendors { + @each $vendor in $vendors { + #{"-" + $vendor + "-" + $prop}: #{$value}; + } + } + // Dump regular property anyway + #{$prop}: #{$value}; + } +} + + +@include keyframes(passing-through) { + + 0% { + opacity: 0; + @include prefix((transform: translateY(40px))); + } + + 30%, 70% { + opacity: 1; + @include prefix((transform: translateY(0px))); + } + + 100% { + opacity: 0; + @include prefix((transform: translateY(-40px))); + } +} + + +@include keyframes(slide-in) { + + 0% { + opacity: 0; + @include prefix((transform: translateY(40px))); + } + + 30% { + opacity: 1; + @include prefix((transform: translateY(0px))); + } +} + + + +@include keyframes(pulse) { + + 0% { @include prefix((transform: scale(1))); } + 10% { @include prefix((transform: scale(1.1))); } + 20% { @include prefix((transform: scale(1))); } + +} + + + +.dropzone, .dropzone * { + box-sizing: border-box; +} +.dropzone { + + $image-size: 120px; + + $image-border-radius: 20px; + + &.dz-clickable { + cursor: pointer; + + * { + cursor: default; + } + .dz-message { + &, * { + cursor: pointer; + } + } + } + + min-height: 150px; + border: 2px solid rgba(0, 0, 0, 0.3); + background: white; + padding: 20px 20px; + + &.dz-started { + .dz-message { + display: none; + } + } + + &.dz-drag-hover { + border-style: solid; + .dz-message { + opacity: 0.5; + } + } + .dz-message { + text-align: center; + margin: 2em 0; + + + } + + + + .dz-preview { + position: relative; + display: inline-block; + + vertical-align: top; + + margin: 16px; + min-height: 100px; + + &:hover { + // Making sure that always the hovered preview element is on top + z-index: 1000; + .dz-details { + opacity: 1; + } + } + + &.dz-file-preview { + + .dz-image { + border-radius: $image-border-radius; + background: #999; + background: linear-gradient(to bottom, #eee, #ddd); + } + + .dz-details { + opacity: 1; + } + } + + &.dz-image-preview { + background: white; + .dz-details { + @include prefix((transition: opacity 0.2s linear)); + } + } + + .dz-remove { + font-size: 14px; + text-align: center; + display: block; + cursor: pointer; + border: none; + &:hover { + text-decoration: underline; + } + } + + &:hover .dz-details { + opacity: 1; + } + .dz-details { + $background-color: #444; + + z-index: 20; + + position: absolute; + top: 0; + left: 0; + + opacity: 0; + + font-size: 13px; + min-width: 100%; + max-width: 100%; + padding: 2em 1em; + text-align: center; + color: rgba(0, 0, 0, 0.9); + + $width: 120px; + + line-height: 150%; + + .dz-size { + margin-bottom: 1em; + font-size: 16px; + } + + .dz-filename { + + white-space: nowrap; + + &:hover { + span { + border: 1px solid rgba(200, 200, 200, 0.8); + background-color: rgba(255, 255, 255, 0.8); + } + } + &:not(:hover) { + span { + border: 1px solid transparent; + } + overflow: hidden; + text-overflow: ellipsis; + } + + } + + .dz-filename, .dz-size { + span { + background-color: rgba(255, 255, 255, 0.4); + padding: 0 0.4em; + border-radius: 3px; + } + } + + } + + &:hover { + .dz-image { + // opacity: 0.8; + img { + @include prefix((transform: scale(1.05, 1.05))); // Getting rid of that white bleed-in + @include prefix((filter: blur(8px)), webkit); // Getting rid of that white bleed-in + } + } + } + .dz-image { + border-radius: $image-border-radius; + overflow: hidden; + width: $image-size; + height: $image-size; + position: relative; + display: block; + z-index: 10; + + img { + display: block; + } + } + + + &.dz-success { + .dz-success-mark { + @include prefix((animation: passing-through 3s cubic-bezier(0.770, 0.000, 0.175, 1.000))); + } + } + &.dz-error { + .dz-error-mark { + opacity: 1; + @include prefix((animation: slide-in 3s cubic-bezier(0.770, 0.000, 0.175, 1.000))); + } + } + + + .dz-success-mark, .dz-error-mark { + + $image-height: 54px; + $image-width: 54px; + + pointer-events: none; + + opacity: 0; + z-index: 500; + + position: absolute; + display: block; + top: 50%; + left: 50%; + margin-left: -($image-width/2); + margin-top: -($image-height/2); + + svg { + display: block; + width: $image-width; + height: $image-height; + } + } + + + &.dz-processing .dz-progress { + opacity: 1; + @include prefix((transition: all 0.2s linear)); + } + &.dz-complete .dz-progress { + opacity: 0; + @include prefix((transition: opacity 0.4s ease-in)); + } + + &:not(.dz-processing) { + .dz-progress { + @include prefix((animation: pulse 6s ease infinite)); + } + } + .dz-progress { + + opacity: 1; + z-index: 1000; + + pointer-events: none; + position: absolute; + height: 16px; + left: 50%; + top: 50%; + margin-top: -8px; + + width: 80px; + margin-left: -40px; + + // border: 2px solid #333; + background: rgba(255, 255, 255, 0.9); + + // Fix for chrome bug: https://code.google.com/p/chromium/issues/detail?id=157218 + -webkit-transform: scale(1); + + + border-radius: 8px; + + overflow: hidden; + + .dz-upload { + background: #333; + background: linear-gradient(to bottom, #666, #444); + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 0; + @include prefix((transition: width 300ms ease-in-out)); + } + + } + + &.dz-error { + .dz-error-message { + display: block; + } + &:hover .dz-error-message { + opacity: 1; + pointer-events: auto; + } + } + + .dz-error-message { + $width: $image-size + 20px; + $color: rgb(190, 38, 38); + + pointer-events: none; + z-index: 1000; + position: absolute; + display: block; + display: none; + opacity: 0; + @include prefix((transition: opacity 0.3s ease)); + border-radius: 8px; + font-size: 13px; + top: $image-size + 10px; + left: -10px; + width: $width; + background: $color; + background: linear-gradient(to bottom, $color, darken($color, 5%)); + padding: 0.5em 1.2em; + color: white; + + // The triangle pointing up + &:after { + content: ''; + position: absolute; + top: -6px; + left: $width / 2 - 6px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid $color; + } + } + + } +} + + diff --git a/modules/files/site/files/scss/files.scss b/modules/files/site/files/scss/files.scss new file mode 100644 index 0000000..836fcfd --- /dev/null +++ b/modules/files/site/files/scss/files.scss @@ -0,0 +1,63 @@ +.uploaded-files { + table { + width: 100%; + border-collapse: collapse; + border: 1px solid black; + + th { + padding: 3px 0 3px 5px; + } + + td { + padding: 3px 0 3px 5px; + } + } + + a.button{ + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; + &:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; + } + } +} + +.upload-files { + .center { + text-align: center; + padding: 10px; + } + a.button{ + margin: auto; + width: 100px; + + color: black; + text-decoration: none; + border: solid 1px #999; + background-color: #ddd; + padding: 2px 4px 2px 4px; + &:hover { + color: black; + border: solid 1px #06f; + background-color: #cff; + } + } +} + + +/******************* Drop Zone *******************/ + +.dropzone { + width: 100%; + border: 2px dashed blue; + border-radius: 3px; + text-align: center; + padding-top: 15px; + padding-bottom: 15px; +} + diff --git a/src/kernel/form/cms_editor.e b/src/kernel/form/cms_editor.e index 542feb9..189edef 100644 --- a/src/kernel/form/cms_editor.e +++ b/src/kernel/form/cms_editor.e @@ -9,38 +9,38 @@ deferred class feature -- Initialisation - load_assets : STRING - -- Loads all assest needed to show the editor + load_assets: STRING + -- Loads all assest needed to show the editor. deferred end feature -- Javascript - javascript_replace_textarea (a_textarea : WSF_FORM_TEXTAREA): STRING - -- Javascript code that replaces a textarea with the editor. The editor instance should be saved in editor_variable + javascript_replace_textarea (a_textarea: WSF_FORM_TEXTAREA): STRING + -- Javascript code that replaces a textarea with the editor. The editor instance should be saved in editor_variable. deferred end - javascript_restore_textarea (a_textarea : WSF_FORM_TEXTAREA): STRING + javascript_restore_textarea (a_textarea: WSF_FORM_TEXTAREA): STRING -- Javascript code that restores a textarea deferred end - javascript_textarea_to_editor (a_textarea : WSF_FORM_TEXTAREA): STRING - -- Javascript code to display the textarea as a WYSIWIG editor as soon as the document is loaded + javascript_textarea_to_editor (a_textarea: WSF_FORM_TEXTAREA): STRING + -- Javascript code to display the textarea as a WYSIWIG editor as soon as the document is loaded. do Result := javascript_ready (javascript_replace_textarea (a_textarea)) end - javascript_textarea_to_editor_if_selected (a_textarea: WSF_FORM_TEXTAREA; a_select_field : WSF_FORM_SELECT; a_value : STRING) : STRING - -- Javascript code to display the textarea as a WYSIWIG editor if a_select_field has a_value + javascript_textarea_to_editor_if_selected (a_textarea: WSF_FORM_TEXTAREA; a_select_field: WSF_FORM_SELECT; a_value: STRING): STRING + -- Javascript code to display the textarea as a WYSIWIG editor if a_select_field has a_value, local initial_replace_code, on_select_replace_code: STRING do - -- Javascript that replaces the textarea if a_value is selected at load time + -- Javascript that replaces the textarea if a_value is selected at load time initial_replace_code := javascript_ready (javascript_if_selected (a_select_field, a_value, javascript_replace_textarea (a_textarea))) - -- Javascript code that replaces the textarea as soon as value is selected at a_select_field + -- Javascript code that replaces the textarea as soon as value is selected at a_select_field on_select_replace_code := javascript_ready( javascript_init_editor_variable (a_textarea) + javascript_on_select (a_select_field, a_value, @@ -55,54 +55,56 @@ feature -- Javascript Result := initial_replace_code + " " + on_select_replace_code end - javascript_init_editor_variable (a_textarea : WSF_FORM_TEXTAREA) : STRING - -- Returns the javascript code that initializes a local variable to store the editor instance + javascript_init_editor_variable (a_textarea: WSF_FORM_TEXTAREA): STRING + -- Returns the javascript code that initializes a local variable to store the editor instance. do Result := "var " + editor_variable (a_textarea) + "; " end - javascript_if_selected (a_select_field : WSF_FORM_SELECT; a_value : STRING; a_code : STRING) : STRING - -- Javascript that executes a_code if a_value is selected at a_select_field + javascript_if_selected (a_select_field: WSF_FORM_SELECT; a_value: STRING; a_code: STRING): STRING + -- Javascript that executes a_code if a_value is selected at a_select_field. do Result := "if($('#" + field_id (a_select_field) + "').val() == %"" + a_value + "%"){ " + a_code + " }" end - javascript_ready (a_code : STRING) : STRING - -- Wraps the given javascript code with a ready statement, such that it's executed when the document has loaded + javascript_ready (a_code: STRING): STRING + -- Wraps the given javascript code with a ready statement, + -- such that it's executed when the document has loaded. do Result := "$(function() { " + a_code + " });" end - javascript_on_select (a_select_field : WSF_FORM_SELECT; a_value : STRING; a_then : STRING; a_else : STRING) : STRING - -- Javascript code that executes a_then if at the given select_field the given string value is selected, otherwise it executes a_else + javascript_on_select (a_select_field: WSF_FORM_SELECT; a_value: STRING; a_then: STRING; a_else: STRING): STRING + -- Javascript code that executes `a_then' if at the given `a_select_field' + -- the given string `a_value' is selected, otherwise it executes `a_else'. do - Result := "$('#" + field_id (a_select_field) + "').change(function(){" + - javascript_if_selected (a_select_field, a_value, a_then) + - "else{" + - a_else + - "}" + - "});" + Result := "$('#" + field_id (a_select_field) + "').change(function(){" + + javascript_if_selected (a_select_field, a_value, a_then) + + "else{" + + a_else + + "}" + + "});" end feature -- Helper - field_id(a_select_field : WSF_FORM_SELECT) : STRING - -- Returns the id of the given field + field_id (a_select_field: WSF_FORM_SELECT): STRING + -- Id of the given field. do - if attached a_select_field.css_id as a_id then + if attached a_select_field.css_id as a_id then Result := a_id else Result := a_select_field.name + "-select" end end - editor_variable (a_textarea : WSF_FORM_TEXTAREA) : STRING + editor_variable (a_textarea: WSF_FORM_TEXTAREA): STRING -- Returns the variable name that stores the editor instance of the given textarea do Result := "cms_ckeditor_" + a_textarea.name end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/kernel/form/cms_editor_ckeditor.e b/src/kernel/form/cms_editor_ckeditor.e index 0fb0e59..e758e80 100644 --- a/src/kernel/form/cms_editor_ckeditor.e +++ b/src/kernel/form/cms_editor_ckeditor.e @@ -12,7 +12,7 @@ inherit feature -- Initialisation - load_assets : STRING + load_assets: STRING -- do Result := "" @@ -20,23 +20,25 @@ feature -- Initialisation feature -- Javascript - javascript_replace_textarea (a_textarea : WSF_FORM_TEXTAREA) : STRING + javascript_replace_textarea (a_textarea: WSF_FORM_TEXTAREA): STRING -- do - -- Replaces the textarea with an editor instance. Save the instance in a variable + -- Replaces the textarea with an editor instance. + -- Save the instance in a variable. Result := "$(%"textarea[name="+ a_textarea.name +"]%").each(function() {" Result.append (editor_variable (a_textarea) + " = CKEDITOR.replace(this);") Result.append ("});") end - javascript_restore_textarea (a_textarea : WSF_FORM_TEXTAREA) : STRING + javascript_restore_textarea (a_textarea: WSF_FORM_TEXTAREA): STRING -- do - -- Replaces the textarea with an editor instance. Save the instance in a variable + -- Replaces the textarea with an editor instance. + -- Save the instance in a variable. Result := "if (" + editor_variable (a_textarea) + " != undefined) " + editor_variable (a_textarea) + ".destroy();" end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/cms_response.e b/src/service/response/cms_response.e index cd05e1e..c5d0953 100644 --- a/src/service/response/cms_response.e +++ b/src/service/response/cms_response.e @@ -282,7 +282,9 @@ feature -- Head customization local s: STRING_8 do - s := " tag. + local + s: STRING_8 + do + create s.make_from_string ("") + add_additional_head_line (s, True) + end + add_javascript_url (a_src: STRING) local s: STRING_8 do - s := "" + create s.make_from_string ("") add_additional_head_line (s, False) end @@ -302,7 +317,9 @@ feature -- Head customization local s: STRING_8 do - s := "" + create s.make_from_string ("") add_additional_head_line (s, True) end diff --git a/src/theme/cms_html_page.e b/src/theme/cms_html_page.e index 3131b16..b026e5b 100644 --- a/src/theme/cms_html_page.e +++ b/src/theme/cms_html_page.e @@ -144,12 +144,17 @@ feature -- Element change title := s end + add_additional_head_line (s: READABLE_STRING_8) + do + head_lines.extend (s) + end + add_meta_name_content (a_name: STRING; a_content: STRING) local s: STRING_8 do s := "" - head_lines.extend (s) + add_additional_head_line (s) end add_meta_http_equiv (a_http_equiv: STRING; a_content: STRING) @@ -157,39 +162,56 @@ feature -- Element change s: STRING_8 do s := "" - head_lines.extend (s) + add_additional_head_line (s) end add_style (a_href: STRING; a_media: detachable STRING) local s: STRING_8 do - s := "") - head_lines.extend (s) + add_additional_head_line (s) + end + + add_style_content (a_style_content: STRING) + -- Add style content `a_style_content' in the head, using ") + add_additional_head_line (s) end add_javascript_url (a_src: STRING) local s: STRING_8 do - s := "" - head_lines.extend (s) + create s.make_from_string ("") + add_additional_head_line (s) end add_javascript_content (a_script: STRING) local s: STRING_8 do - s := "" - head_lines.extend (s) + create s.make_from_string ("") + add_additional_head_line (s) end note - copyright: "2011-2014, Jocelyn Fiat, Eiffel Software and others" + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/tools/roc.bat b/tools/roc.bat index 8d8f501..5c2089e 100644 --- a/tools/roc.bat +++ b/tools/roc.bat @@ -30,7 +30,7 @@ set ROC_TOOL_PATH=%~dp0 goto START :START -echo Calling %ROC_TOOL_PATH%roc.exe %* +rem echo Calling %ROC_TOOL_PATH%roc.exe %* call %ROC_TOOL_PATH%roc.exe %* goto END