diff --git a/gui/app/components/folder/documents-list.js b/gui/app/components/folder/documents-list.js index 88100bed..c8194be7 100644 --- a/gui/app/components/folder/documents-list.js +++ b/gui/app/components/folder/documents-list.js @@ -14,43 +14,13 @@ import Ember from 'ember'; export default Ember.Component.extend({ folderService: Ember.inject.service('folder'), moveTarget: null, - emptyState: Ember.computed('documents', function() { - return this.get('documents.length') === 0; - }), didReceiveAttrs() { this._super(...arguments); - this.set('canCreate', this.get('permissions.documentAdd')); this.set('deleteTargets', this.get('folders').rejectBy('id', this.get('folder.id'))); }, - didUpdateAttrs() { - this._super(...arguments); - - this.setupAddWizard(); - }, - - didInsertElement() { - this._super(...arguments); - - this.setupAddWizard(); - }, - - setupAddWizard() { - Ember.run.schedule('afterRender', () => { - $('.start-document:not(.start-document-empty-state)').off('.hoverIntent'); - - $('.start-document:not(.start-document-empty-state)').hoverIntent({interval: 100, over: function() { - // in - $(this).find('.start-button').velocity("transition.slideDownIn", {duration: 300}); - }, out: function() { - // out - $(this).find('.start-button').velocity("transition.slideUpOut", {duration: 300}); - } }); - }); - }, - actions: { selectDocument(documentId) { let doc = this.get('documents').findBy('id', documentId); @@ -65,39 +35,6 @@ export default Ember.Component.extend({ } this.set('selectedDocuments', list); - }, - - onDelete() { - this.get("onDeleteSpace")(); - }, - - onImport() { - this.get('onImport')(); - }, - - onShowDocumentWizard(docId) { - if ($("#new-document-wizard").is(':visible') && this.get('docId') === docId) { - this.send('onHideDocumentWizard'); - return; - } - - this.set('docId', docId); - - if (docId === '') { - $("#new-document-wizard").insertAfter('#wizard-placeholder'); - } else { - $("#new-document-wizard").insertAfter(`#document-${docId}`); - } - - $("#new-document-wizard").velocity("transition.slideDownIn", { duration: 300, complete: - function() { - $("#new-document-name").focus(); - }}); - }, - - onHideDocumentWizard() { - $("#new-document-wizard").insertAfter('#wizard-placeholder'); - $("#new-document-wizard").velocity("transition.slideUpOut", { duration: 300 }); - } + } } }); diff --git a/gui/app/components/folder/folder-heading.js b/gui/app/components/folder/space-heading.js similarity index 100% rename from gui/app/components/folder/folder-heading.js rename to gui/app/components/folder/space-heading.js diff --git a/gui/app/components/folder/folder-toolbar.js b/gui/app/components/folder/space-toolbar.js similarity index 95% rename from gui/app/components/folder/folder-toolbar.js rename to gui/app/components/folder/space-toolbar.js index 7b1294b3..93e85f3b 100644 --- a/gui/app/components/folder/folder-toolbar.js +++ b/gui/app/components/folder/space-toolbar.js @@ -81,6 +81,10 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, { } else { this.addTooltip(document.getElementById("space-pin-button")); } + + if (this.get('permissions.documentAdd')) { + this.addTooltip(document.getElementById("document-add-button")); + } } }, @@ -170,6 +174,10 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, { this.attrs.onMoveDocument(this.get('moveFolderId')); return true; + }, + + onStartDocument() { + this.attrs.onStartDocument(); } } }); diff --git a/gui/app/components/folder/space-view.js b/gui/app/components/folder/space-view.js new file mode 100644 index 00000000..15b8cced --- /dev/null +++ b/gui/app/components/folder/space-view.js @@ -0,0 +1,91 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; +import NotifierMixin from '../../mixins/notifier'; +import TooltipMixin from '../../mixins/tooltip'; +import AuthMixin from '../../mixins/auth'; + +const { + inject: { service } +} = Ember; + +export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, { + router: service(), + documentService: service('document'), + folderService: service('folder'), + localStorage: service('localStorage'), + selectedDocuments: [], + hasSelectedDocuments: Ember.computed.gt('selectedDocuments.length', 0), + showStartDocument: false, + + actions: { + onMoveDocument(folder) { + let self = this; + let documents = this.get('selectedDocuments'); + + documents.forEach(function (documentId) { + self.get('documentService').getDocument(documentId).then(function (doc) { + doc.set('folderId', folder); + doc.set('selected', !doc.get('selected')); + self.get('documentService').save(doc).then(function () { + self.attrs.onRefresh(); + }); + }); + }); + + this.set('selectedDocuments', []); + this.send("showNotification", "Moved"); + }, + + onDeleteDocument() { + let documents = this.get('selectedDocuments'); + let self = this; + let promises = []; + + documents.forEach(function (document, index) { + promises[index] = self.get('documentService').deleteDocument(document); + }); + + Ember.RSVP.all(promises).then(() => { + let documents = this.get('documents'); + documents.forEach(function (document) { + document.set('selected', false); + }); + this.set('documents', documents); + + this.set('selectedDocuments', []); + this.send("showNotification", "Deleted"); + this.attrs.onRefresh(); + }); + }, + + onDeleteSpace() { + this.get('folderService').delete(this.get('folder.id')).then(() => { /* jshint ignore:line */ + this.showNotification("Deleted"); + this.get('localStorage').clearSessionItem('folder'); + this.get('router').transitionTo('application'); + }); + }, + + onImport() { + // this.attrs.onRefresh(); + }, + + onStartDocument() { + this.set('showStartDocument', !this.get('showStartDocument')); + }, + + onHideStartDocument() { + this.set('showStartDocument', false); + } + } +}); diff --git a/gui/app/components/folder/start-document.js b/gui/app/components/folder/start-document.js index 293528cb..a705e932 100644 --- a/gui/app/components/folder/start-document.js +++ b/gui/app/components/folder/start-document.js @@ -14,54 +14,66 @@ import NotifierMixin from '../../mixins/notifier'; const { computed, + inject: { service } } = Ember; export default Ember.Component.extend(NotifierMixin, { - localStorage: Ember.inject.service(), - appMeta: Ember.inject.service(), - templateService: Ember.inject.service('template'), - canEditTemplate: "", + localStorage: service(), + appMeta: service(), + templateService: service('template'), importedDocuments: [], savedTemplates: [], - drop: null, - newDocumentName: 'New Document', + dropzone: null, + newDocumentName: '', newDocumentNameMissing: computed.empty('newDocumentName'), - didInsertElement() { - this.setupImport(); - }, - didReceiveAttrs() { + this._super(...arguments); + this.setupTemplates(); + + Ember.run.schedule('afterRender', ()=> { + this.setupImport(); + }); }, willDestroyElement() { - if (is.not.null(this.get('drop'))) { - this.get('drop').destroy(); - this.set('drop', null); + this._super(...arguments); + + if (is.not.null(this.get('dropzone'))) { + this.get('dropzone').destroy(); + this.set('dropzone', null); } }, setupTemplates() { let templates = this.get('templates'); - let emptyTemplate = { - id: "0", - title: "Empty", - description: "An empty canvas for your words", - layout: "doc", - locked: true - }; + if (is.undefined(templates.findBy('id', '0'))) { + let emptyTemplate = { + id: "0", + title: "Blank", + description: "An empty canvas for your words", + layout: "doc", + locked: true + }; + + templates.unshiftObject(emptyTemplate); + } - templates.unshiftObject(emptyTemplate); this.set('savedTemplates', templates); + + Ember.run.schedule('afterRender', () => { + $('#new-document-name').select(); + }); }, setupImport() { + console.log("setting up import"); // already done init? - if (is.not.null(this.get('drop'))) { - this.get('drop').destroy(); - this.set('drop', null); + if (is.not.null(this.get('dropzone'))) { + this.get('dropzone').destroy(); + this.set('dropzone', null); } let self = this; @@ -70,9 +82,7 @@ export default Ember.Component.extend(NotifierMixin, { let importUrl = `${url}/import/folder/${folderId}`; let dzone = new Dropzone("#import-document-button", { - headers: { - 'Authorization': 'Bearer ' + self.get('session.session.content.authenticated.token') - }, + headers: { 'Authorization': 'Bearer ' + self.get('session.session.content.authenticated.token') }, url: importUrl, method: "post", paramName: 'attachment', @@ -90,7 +100,7 @@ export default Ember.Component.extend(NotifierMixin, { }); this.on("error", function (x) { - console.log("Conversion failed for ", x.name, " obj ", x); // eslint-disable-line no-console + console.log("Conversion failed for", x.name, x); // eslint-disable-line no-console }); this.on("queuecomplete", function () {}); @@ -105,12 +115,12 @@ export default Ember.Component.extend(NotifierMixin, { dzone.removeFile(file); }); - this.set('drop', dzone); + this.set('dropzone', dzone); }, actions: { - onHideDocumentWizard() { - this.get('onHideDocumentWizard')(); + onHideStartDocument() { + this.get('onHideStartDocument')(); }, editTemplate(template) { @@ -120,7 +130,13 @@ export default Ember.Component.extend(NotifierMixin, { }, startDocument(template) { - this.send("showNotification", "Creating"); + if (this.get('newDocumentNameMissing')) { + this.$("#new-document-name").addClass('error').focus(); + return; + } + + this.$("#new-document-name").removeClass('error'); + this.send("showNotification", "Creating"); this.get('templateService').importSavedTemplate(this.folder.get('id'), template.id, this.get('newDocumentName')).then((document) => { this.get('router').transitionTo('document', this.get('folder.id'), this.get('folder.slug'), document.get('id'), document.get('slug')); @@ -130,26 +146,30 @@ export default Ember.Component.extend(NotifierMixin, { }, onDocumentImporting(filename) { + if (this.isDestroyed) { return; } + this.send("showNotification", `Importing ${filename}`); - this.get('onHideDocumentWizard')(); + this.get('onHideStartDocument')(); let documents = this.get('importedDocuments'); documents.push(filename); + + if (this.isDestroyed) { return; } this.set('importedDocuments', documents); }, onDocumentImported(filename /*, document*/ ) { + if (this.isDestroyed) { return; } + this.send("showNotification", `${filename} ready`); let documents = this.get('importedDocuments'); documents.pop(filename); + + if (this.isDestroyed) { return; } + this.set('importedDocuments', documents); - this.get('onImport')(); - - if (documents.length === 0) { - // this.get('showDocument')(this.get('folder'), document); - } }, } }); diff --git a/gui/app/pods/folder/index/controller.js b/gui/app/pods/folder/index/controller.js index f0c8f376..6c5ec576 100644 --- a/gui/app/pods/folder/index/controller.js +++ b/gui/app/pods/folder/index/controller.js @@ -10,58 +10,19 @@ // https://documize.com import Ember from 'ember'; -import NotifierMixin from '../../../mixins/notifier'; -export default Ember.Controller.extend(NotifierMixin, { - documentService: Ember.inject.service('document'), - folderService: Ember.inject.service('folder'), - localStorage: Ember.inject.service('localStorage'), - selectedDocuments: [], - hasSelectedDocuments: Ember.computed.gt('selectedDocuments.length', 0), +const { + inject: { service } +} = Ember; + +export default Ember.Controller.extend({ + documentService: service('document'), + folderService: service('folder'), + localStorage: service('localStorage'), queryParams: ['tab'], tab: 'index', actions: { - onMoveDocument(folder) { - let self = this; - let documents = this.get('selectedDocuments'); - - documents.forEach(function (documentId) { - self.get('documentService').getDocument(documentId).then(function (doc) { - doc.set('folderId', folder); - doc.set('selected', !doc.get('selected')); - self.get('documentService').save(doc).then(function () { - self.get('target._routerMicrolib').refresh(); - }); - }); - }); - - this.set('selectedDocuments', []); - this.send("showNotification", "Moved"); - }, - - onDeleteDocument() { - let documents = this.get('selectedDocuments'); - let self = this; - let promises = []; - - documents.forEach(function (document, index) { - promises[index] = self.get('documentService').deleteDocument(document); - }); - - Ember.RSVP.all(promises).then(() => { - let documents = this.get('model.documents'); - documents.forEach(function (document) { - document.set('selected', false); - }); - this.set('model.documents', documents); - - this.set('selectedDocuments', []); - this.send("showNotification", "Deleted"); - this.get('target._routerMicrolib').refresh(); - }); - }, - onAddSpace(payload) { let self = this; this.showNotification("Added"); @@ -72,15 +33,7 @@ export default Ember.Controller.extend(NotifierMixin, { }); }, - onDeleteSpace() { - this.get('folderService').delete(this.get('model.folder.id')).then(() => { /* jshint ignore:line */ - this.showNotification("Deleted"); - this.get('localStorage').clearSessionItem('folder'); - this.transitionToRoute('application'); - }); - }, - - onImport() { + onRefresh() { this.get('target._routerMicrolib').refresh(); } } diff --git a/gui/app/pods/folder/index/route.js b/gui/app/pods/folder/index/route.js index 3ec56e14..9f16fed7 100644 --- a/gui/app/pods/folder/index/route.js +++ b/gui/app/pods/folder/index/route.js @@ -21,7 +21,12 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, { permissions: this.modelFor('folder').permissions, folders: this.modelFor('folder').folders, documents: this.modelFor('folder').documents, - templates: this.modelFor('folder').templates + templates: this.modelFor('folder').templates, + showStartDocument: false, }); + }, + + activate() { + this.set('model.showStartDocument', false); } }); diff --git a/gui/app/pods/folder/index/template.hbs b/gui/app/pods/folder/index/template.hbs index efce6144..932b3f8e 100644 --- a/gui/app/pods/folder/index/template.hbs +++ b/gui/app/pods/folder/index/template.hbs @@ -1,21 +1,20 @@ {{#layout/zone-container}} + {{#layout/zone-sidebar}} {{folder/sidebar-zone folders=model.folders folder=model.folder permissions=model.permissions tab=tab onAddSpace=(action 'onAddSpace')}} {{/layout/zone-sidebar}} + {{#layout/zone-content}} - {{folder/folder-heading folder=model.folder permissions=model.permissions}} + {{folder/space-view + folders=model.folders + folder=model.folder + templates=model.templates + permissions=model.permissions + documents=model.documents + onRefresh=(action 'onRefresh')}} - {{folder/folder-toolbar folders=model.folders folder=model.folder - permissions=model.permissions hasSelectedDocuments=hasSelectedDocuments - onDeleteDocument=(action 'onDeleteDocument') onMoveDocument=(action 'onMoveDocument') - onDeleteSpace=(action 'onDeleteSpace')}} - -
- - {{folder/documents-list documents=model.documents folders=model.folders folder=model.folder templates=model.templates - permissions=model.permissions selectedDocuments=(mut selectedDocuments) - onDeleteSpace=(action 'onDeleteSpace') onImport=(action 'onImport')}} {{/layout/zone-content}} + {{/layout/zone-container}} \ No newline at end of file diff --git a/gui/app/styles/color.scss b/gui/app/styles/color.scss index 04f63882..0b0796b9 100644 --- a/gui/app/styles/color.scss +++ b/gui/app/styles/color.scss @@ -37,16 +37,15 @@ $color-link: #0092d3; $color-border: #f3f5f8; $color-checkbox: #0092d3; -$color-card-active: #f7fcff; - -$color-nav-button: #f2faff; -$color-nav-button-text: #2667af; -$color-nav-button-border: #dff0f9; $color-sidebar: #f2faff; $color-sidebar-border: #dff0f9; $color-sidebar-text: $color-off-black; $color-sidebar-link: $color-link; +$color-selected-item: $color-sidebar; +$color-nav-button: $color-sidebar; +$color-nav-button-text: #2667af; +$color-nav-button-border: #dff0f9; // Color utility classes for direct usage in HTML .color-white { diff --git a/gui/app/styles/view/folder/document.scss b/gui/app/styles/view/folder/document.scss index 30748dbb..70fae834 100644 --- a/gui/app/styles/view/folder/document.scss +++ b/gui/app/styles/view/folder/document.scss @@ -22,9 +22,11 @@ } .documents-list { + @include content-container(); + .document-item { - @include content-container(); margin: 0; + padding: 25px 15px; position: relative; transition: 0.3s; @@ -48,7 +50,7 @@ position: absolute; display: none; top: 10px; - right: 20px; + right: 10px; cursor: pointer; > .material-icons { @@ -82,18 +84,8 @@ } } - .wizard-item { - margin: 0; - padding: 0; - } - - .no-wizard-item { - margin: 50px 0; - padding: 0; - } - .selected-card { - background-color: $color-card-active !important; + background-color: $color-selected-item !important; > .checkbox { display: block; @@ -101,8 +93,7 @@ } } -.move-document-options, -.start-document-options { +.move-document-options { height: 200px; overflow-y: auto; margin: 0; diff --git a/gui/app/styles/view/folder/wizard.scss b/gui/app/styles/view/folder/wizard.scss index ea408633..31d4abfe 100644 --- a/gui/app/styles/view/folder/wizard.scss +++ b/gui/app/styles/view/folder/wizard.scss @@ -1,66 +1,5 @@ -.start-document { - @extend .no-select; - height: 60px; - background-color: transparent; - position: relative; - cursor: pointer; - - > .start-button { - display: none; - text-align: center; - padding-top: 20px; - color: $color-green; - font-size: 1rem; - position: relative; - - > .round-button { - opacity: 0.6 !important; - } - - > .label { - display: inline-block; - margin-left: 10px; - } - - > .line { - display: inline-block; - height: 1px; - border: 1px solid $color-green; - width: 100px; - margin: 0 20px 0 20px; - opacity: 0.2; - } - } - - &:first-of-type { - height: 30px; - - > .start-button { - padding-top: 0; - } - } -} - -.start-document-empty-state { - > .start-button { - display: block; - } -} - .new-document-wizard { - @include border-radius(2px); - margin: 0 0 30px 0; - padding: 30px; - border: 1px solid $color-stroke; - background-color: $color-off-white; - display: none; - - .document-name { - font-weight: bold; - font-size: 1.5rem; - margin: 0 0 30px 0; - color: $color-black; - } + @include content-container(); .import-document-button { width: 100%; @@ -68,11 +7,13 @@ margin: 0 0 30px 0; text-align: center; color: $color-gray; - border: 1px solid $color-stroke; cursor: pointer; - font-size: 1rem; + font-size: 1.2rem; line-height: 1.7rem; @include ease-in(); + @extend .no-select; + border: 1px solid $color-stroke; + background-color: $color-off-white; &:hover { border-color: $color-link; @@ -85,74 +26,72 @@ } } - > .list-wrapper { - // height: 440px; - // overflow-y: auto; + > .input-control { + > .list-wrapper { + > .template-list { + margin: 0; + padding: 0; - > .template-list { - margin: 0; - padding: 0; - - > .item { - @include ease-in(); - @include border-radius(4px); - list-style: none; - cursor: pointer; - display: inline-block; - border: 1px solid $color-stroke; - background-color: $color-white; - margin: 0 20px 20px 0; - padding: 12px 0 0 20px; - width: 423px; - height: 60px; - position: relative; - - &:hover { + .item { @include ease-in(); - border-color: $color-link; + @include border-radius(4px); + list-style: none; + cursor: pointer; + display: inline-block; + background-color: $color-blue; + margin: 0 20px 20px 0; + padding: 12px 0 0 20px; + width: 423px; + height: 60px; + position: relative; + + &:hover { + @include ease-in(); + border-color: $color-link; + + > .template-actions { + display: block; + } + } > .template-actions { - display: block; + @include ease-in(); + display: none; + position: absolute; + top: 10px; + right: 8px; + + .material-icons { + color: $color-stroke; + font-size: 1rem; + } } - } - > .template-actions { - @include ease-in(); - display: none; - position: absolute; - top: 10px; - right: 8px; - - .material-icons { - color: $color-stroke; - font-size: 1rem; - } - } - - > .details { - width: 350px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - > .title { - font-size: 1rem; - font-weight: normal; - color: $color-off-black; - letter-spacing: 0.5px; - margin-top: 0; + > .details { + width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - } - > .desc { - color: $color-gray; - font-size: 0.8rem; - margin-top: 5px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + > .title { + font-size: 1rem; + font-weight: normal; + color: $color-white; + letter-spacing: 0.5px; + margin-top: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > .desc { + color: $color-white; + font-size: 0.8rem; + margin-top: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } } diff --git a/gui/app/styles/widget/widget-card.scss b/gui/app/styles/widget/widget-card.scss index 26a743e2..5ec65366 100644 --- a/gui/app/styles/widget/widget-card.scss +++ b/gui/app/styles/widget/widget-card.scss @@ -6,8 +6,8 @@ &:hover { @extend .z-depth-half; - background-color: $color-card-active; - border-color: $color-card-active; + background-color: $color-selected-item; + border-color: $color-selected-item; transition: 0.2s all ease; } } diff --git a/gui/app/styles/widget/widget-selection.scss b/gui/app/styles/widget/widget-selection.scss index 0476c322..aa761381 100644 --- a/gui/app/styles/widget/widget-selection.scss +++ b/gui/app/styles/widget/widget-selection.scss @@ -21,7 +21,7 @@ } > .selected { - background-color: $color-card-active !important; + background-color: $color-selected-item !important; color: $color-primary !important; > i.material-icons { diff --git a/gui/app/templates/components/document/space-category.hbs b/gui/app/templates/components/document/space-category.hbs index a254b1d4..a0b84b94 100644 --- a/gui/app/templates/components/document/space-category.hbs +++ b/gui/app/templates/components/document/space-category.hbs @@ -12,7 +12,7 @@
Category
{{#each selectedCategories as |cat|}} -
{{cat.category}}
+
{{cat.category}}
{{else}} {{#if canAddCategory}} {{#link-to 'folder.settings.category' folder.id folder.slug}}Manage{{/link-to}} diff --git a/gui/app/templates/components/folder/documents-list.hbs b/gui/app/templates/components/folder/documents-list.hbs index 94e81a16..a2ee0aaa 100644 --- a/gui/app/templates/components/folder/documents-list.hbs +++ b/gui/app/templates/components/folder/documents-list.hbs @@ -17,39 +17,6 @@
{{/if}} - {{#if canCreate}} -
-
-
- add -
-
document
-
-
- {{else}} -
- {{/if}}
{{/each}} - -{{folder/start-document folder=folder templates=templates permissions=permissions onImport=(action 'onImport') onHideDocumentWizard=(action 'onHideDocumentWizard')}} - -{{#if emptyState}} - {{#if canCreate}} -
-
-
- add -
-
document
-
-
- {{/if}} -{{/if}} - -
- -{{#if emptyState}} -
delete space
-{{/if}} diff --git a/gui/app/templates/components/folder/folder-heading.hbs b/gui/app/templates/components/folder/space-heading.hbs similarity index 100% rename from gui/app/templates/components/folder/folder-heading.hbs rename to gui/app/templates/components/folder/space-heading.hbs diff --git a/gui/app/templates/components/folder/folder-toolbar.hbs b/gui/app/templates/components/folder/space-toolbar.hbs similarity index 89% rename from gui/app/templates/components/folder/folder-toolbar.hbs rename to gui/app/templates/components/folder/space-toolbar.hbs index 92a7efd5..a6069ddb 100644 --- a/gui/app/templates/components/folder/folder-toolbar.hbs +++ b/gui/app/templates/components/folder/space-toolbar.hbs @@ -41,6 +41,13 @@ {{else}} + {{#if permissions.documentAdd}} +
+ add +
+
+ {{/if}} + {{#if pinState.isPinned}}
favorite @@ -62,7 +69,7 @@ {{#if permissions.spaceOwner}}
-
+
delete
{{/if}} diff --git a/gui/app/templates/components/folder/space-view.hbs b/gui/app/templates/components/folder/space-view.hbs new file mode 100644 index 00000000..c6cb3cf8 --- /dev/null +++ b/gui/app/templates/components/folder/space-view.hbs @@ -0,0 +1,17 @@ +{{folder/space-heading folder=folder permissions=permissions}} + +{{folder/space-toolbar folders=folders folder=folder + permissions=permissions hasSelectedDocuments=hasSelectedDocuments + onDeleteDocument=(action 'onDeleteDocument') onMoveDocument=(action 'onMoveDocument') + onDeleteSpace=(action 'onDeleteSpace') onStartDocument=(action 'onStartDocument')}} + +
+ +{{#if showStartDocument}} + {{folder/start-document folder=folder templates=templates permissions=permissions + onImport=(action 'onImport') onHideStartDocument=(action 'onHideStartDocument')}} +{{else}} + {{folder/documents-list documents=documents folders=folders folder=folder + templates=templates permissions=permissions selectedDocuments=(mut selectedDocuments) + onImport=(action 'onImport')}} +{{/if}} \ No newline at end of file diff --git a/gui/app/templates/components/folder/start-document.hbs b/gui/app/templates/components/folder/start-document.hbs index a580ee4a..c57d4ad1 100644 --- a/gui/app/templates/components/folder/start-document.hbs +++ b/gui/app/templates/components/folder/start-document.hbs @@ -1,35 +1,42 @@
-
- {{input type="text" id="new-document-name" value=newDocumentName class=(if newDocumentNameMissing 'document-name error-inline' 'document-name mousetrap') placeholder="Name" autocomplete="off"}} +
+ +
Provide a concise name for the new document
+ {{focus-input type="text" id="new-document-name" value=newDocumentName class=(if newDocumentNameMissing 'document-name error-inline' 'document-name mousetrap') autocomplete="off"}}
-
+
close
-
- Drag-drop or click to select .doc, .docx, .md, .markdown files +
+ +
Start a blank document or pick a template
+ +
+
    + {{#each savedTemplates key="id" as |template|}} +
  • + {{#if permissions.documentTemplate}} + {{#unless template.locked}} +
    + mode_edit +
    + {{/unless}} + {{/if}} +
    +
    {{template.title}}
    +
    {{template.description}}
    +
    +
  • + {{/each}} +
+
-
-
    - {{#each savedTemplates key="id" as |template|}} -
  • - {{#if permissions.documentTemplate}} - {{#unless template.locked}} -
    - mode_edit -
    - {{/unless}} - {{/if}} -
    -
    {{template.title}}
    -
    {{template.description}}
    -
    -
  • - {{/each}} -
+
+ Alternatively, click to select or drag-drop files
(doc, docx, md, markdown)
diff --git a/gui/vendor/dropzone.js b/gui/vendor/dropzone.js index 895b055a..72bb1e6a 100755 --- a/gui/vendor/dropzone.js +++ b/gui/vendor/dropzone.js @@ -1,4 +1,3 @@ - /* * * More info at [www.dropzonejs.com](http://www.dropzonejs.com) @@ -25,1679 +24,2509 @@ * */ -(function() { - 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"; +// 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 + on(event, fn) { + this._callbacks = this._callbacks || {}; + // Create namespace for this event + if (!this._callbacks[event]) { + this._callbacks[event] = []; + } + this._callbacks[event].push(fn); + return this; + } + + + emit(event, ...args) { + this._callbacks = this._callbacks || {}; + let callbacks = this._callbacks[event]; + + if (callbacks) { + for (let callback of callbacks) { + callback.apply(this, args); + } + } + + return this; + } + + // 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. + off(event, fn) { + if (!this._callbacks || (arguments.length === 0)) { + this._callbacks = {}; + return this; + } + + // specific event + let callbacks = this._callbacks[event]; + if (!callbacks) { + return this; + } + + // remove all handlers + if (arguments.length === 1) { + delete this._callbacks[event]; + return this; + } + + // remove specific handler + for (let i = 0; i < callbacks.length; i++) { + let callback = callbacks[i]; + if (callback === fn) { + callbacks.splice(i, 1); + break; + } + } + + return this; + } + } + + class Dropzone extends Emitter { + static initClass() { + + // Exposing the emitter class, mainly for tests + this.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() { }); + + */ + this.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" + ]; + + + this.prototype.defaultOptions = { + /** + * Has to be specified on elements other than form (or when the form + * doesn't have an `action` attribute). You can also + * provide a function that will be called with `files` and + * must return the url (since `v3.12.0`) + */ + url: null, + + /** + * Can be changed to `"put"` if necessary. You can also provide a function + * that will be called with `files` and must return the method (since `v3.12.0`). + */ + method: "post", + + /** + * Will be set on the XHRequest. + */ + withCredentials: false, + + /** + * The timeout for the XHR requests in milliseconds (since `v4.4.0`). + */ + timeout: 30000, + + /** + * How many file uploads to process in parallel (See the + * Enqueuing file uploads* documentation section for more info) + */ + parallelUploads: 2, + + /** + * Whether to send multiple files in one request. If + * this it set to true, then the fallback file input element will + * have the `multiple` attribute as well. This option will + * also trigger additional events (like `processingmultiple`). See the events + * documentation section for more information. + */ + uploadMultiple: false, + + /** + * If not `null` defines how many files this Dropzone handles. If it exceeds, + * the event `maxfilesexceeded` will be called. The dropzone element gets the + * class `dz-max-files-reached` accordingly so you can provide visual feedback. + */ + maxFilesize: 256, + + /** + * The name of the file param that gets transferred. + * **NOTE**: If you have the option `uploadMultiple` set to `true`, then + * Dropzone will append `[]` to the name. + */ + paramName: "file", + + /** + * Whether thumbnails for images should be generated + */ + createImageThumbnails: true, + + /** + * In MB. When the filename exceeds this limit, the thumbnail will not be generated. + */ + maxThumbnailFilesize: 10, + + /** + * If `null`, the ratio of the image will be used to calculate it. + */ + thumbnailWidth: 120, + + /** + * The same as `thumbnailWidth`. If both are null, images will not be resized. + */ + thumbnailHeight: 120, + + /** + * How the images should be scaled down in case both, `thumbnailWidth` and `thumbnailHeight` are provided. + * Can be either `contain` or `crop`. + */ + thumbnailMethod: 'crop', + + /** + * If set, images will be resized to these dimensions before being **uploaded**. + * If only one, `resizeWidth` **or** `resizeHeight` is provided, the original aspect + * ratio of the file will be preserved. + * + * The `options.transformFile` function uses these options, so if the `transformFile` function + * is overridden, these options don't do anything. + */ + resizeWidth: null, + + /** + * See `resizeWidth`. + */ + resizeHeight: null, + + /** + * The mime type of the resized image (before it gets uploaded to the server). + * If `null` the original mime type will be used. To force jpeg, for example, use `image/jpeg`. + * See `resizeWidth` for more information. + */ + resizeMimeType: null, + + /** + * The quality of the resized images. See `resizeWidth`. + */ + resizeQuality: 0.8, + + /** + * How the images should be scaled down in case both, `resizeWidth` and `resizeHeight` are provided. + * Can be either `contain` or `crop`. + */ + resizeMethod: 'contain', + + /** + * 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: {}, + + /** + * An optional object to send additional headers to the server. Eg: + * `{ "My-Awesome-Header": "header value" }` + */ + headers: null, + + /** + * If `true`, the dropzone element itself will be clickable, if `false` + * nothing will be clickable. + * + * You can also pass an HTML element, a CSS selector (for multiple elements) + * or an array of those. In that case, all of those elements will trigger an + * upload when clicked. + */ + clickable: true, + + /** + * Whether hidden files in directories should be ignored. + */ + ignoreHiddenFiles: true, + + + /** + * The default implementation of `accept` checks the file's mime type or + * extension against this list. This is a comma separated list of mime + * types or file extensions. + * + * Eg.: `image/*,application/pdf,.psd` + * + * If the Dropzone is `clickable` this option will also be used as + * [`accept`](https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept) + * parameter on the hidden file input as well. + */ + 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()`. + * + * See the [enqueuing file uploads](#enqueuing-file-uploads) documentation + * section for more information. + */ + autoProcessQueue: true, + + /** + * If false, files added to the dropzone will not be queued by default. + * You'll have to call `enqueueFile(file)` manually. + */ + autoQueue: true, + + /** + * If `true`, this will add a link to every file preview to remove or cancel (if + * already uploading) the file. The `dictCancelUpload`, `dictCancelUploadConfirmation` + * and `dictRemoveFile` options are used for the wording. + */ + addRemoveLinks: false, + + /** + * Defines where to display the file previews – if `null` the + * Dropzone element itself is used. Can be a plain `HTMLElement` or a CSS + * selector. The element should have the `dropzone-previews` class so + * the previews are displayed properly. + */ + previewsContainer: null, + + /** + * This is the element the hidden input field (which is used when clicking on the + * dropzone to trigger file selection) will be appended to. This might + * be important in case you use frameworks to switch the content of your page. + */ + 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, + + /** + * **Deprecated**. Use `renameFile` instead. + */ + renameFilename: null, + + /** + * A function that is invoked before the file is uploaded to the server and renames the file. + * This function gets the `File` as argument and can use the `file.name`. The actual name of the + * file that gets used during the upload can be accessed through `file.upload.filename`. + */ + renameFile: null, + + /** + * If `true` the fallback will be forced. This is very useful to test your server + * implementations first and make sure that everything works as + * expected without dropzone if you experience problems, and to test + * how your fallbacks will look. + */ + forceFallback: false, + + /** + * 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 you provide a fallback element yourself, or if this option is `null` this will + * be ignored. + */ + dictFallbackText: "Please use the fallback form below to upload your files like in the olden days.", + + /** + * If the filesize is too big. + * `{{filesize}}` and `{{maxFilesize}}` will be replaced with the respective configuration values. + */ + 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. + * `{{statusCode}}` will be replaced with the servers status code. + */ + dictResponseError: "Server responded with {{statusCode}} code.", + + /** + * If `addRemoveLinks` is true, the text to be used for the cancel upload link. + */ + dictCancelUpload: "Cancel upload", + + /** + * If `addRemoveLinks` is true, the text to be used for confirmation when cancelling upload. + */ + dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?", + + /** + * If `addRemoveLinks` is true, 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 if `maxFiles` is st and exceeded. + * The string `{{maxFiles}}` will be replaced by the configuration value. + */ + dictMaxFilesExceeded: "You can not upload any more files.", + + /** + * Allows you to translate the different units. Starting with `tb` for terabytes and going down to + * `b` for bytes. + */ + dictFileSizeUnits: {tb: "TB", gb: "GB", mb: "MB", kb: "KB", b: "b"}, + + /** + * Called when dropzone initialized + * You can add event listeners here + */ + init() {}, + + /** + * A function that gets a [file](https://developer.mozilla.org/en-US/docs/DOM/File) + * and a `done` function as parameters. + * + * If the done function is invoked without arguments, the file is "accepted" and will + * be processed. If you pass an error message, the file is rejected, and the error + * message will be displayed. + * This function will not be called if the file is too big or doesn't match the mime types. + */ + accept(file, done) { + return done(); + }, + + + /** + * Gets called when the browser is not supported. + * The default implementation shows the fallback input field and adds + * a text. + */ + fallback() { + // This code should pass in IE7... :( + let messageElement; + this.element.className = `${this.element.className} dz-browser-not-supported`; + + for (let child of this.element.getElementsByTagName("div")) { + if (/(^| )dz-message($| )/.test(child.className)) { + messageElement = child; + child.className = "dz-message"; // Removes the 'dz-default' class + continue; + } + } + if (!messageElement) { + messageElement = Dropzone.createElement("
"); + this.element.appendChild(messageElement); + } + + let 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()); + }, + + + /** + * Gets called to calculate the thumbnail dimensions. + * + * It gets `file`, `width` and `height` (both may be `null`) as parameters and must return an object containing: + * + * - `srcWidth` & `srcHeight` (required) + * - `trgWidth` & `trgHeight` (required) + * - `srcX` & `srcY` (optional, default `0`) + * - `trgX` & `trgY` (optional, default `0`) + * + * Those values are going to be used by `ctx.drawImage()`. + */ + resize(file, width, height, resizeMethod) { + let info = { + srcX: 0, + srcY: 0, + srcWidth: file.width, + srcHeight: file.height + }; + + let srcRatio = file.width / file.height; + + // Automatically calculate dimensions if not specified + if ((width == null) && (height == null)) { + width = info.srcWidth; + height = info.srcHeight; + } else if ((width == null)) { + width = height * srcRatio; + } else if ((height == null)) { + height = width / srcRatio; + } + + // Make sure images aren't upscaled + width = Math.min(width, info.srcWidth); + height = Math.min(height, info.srcHeight); + + let trgRatio = width / height; + + if ((info.srcWidth > width) || (info.srcHeight > height)) { + // Image is bigger and needs rescaling + if (resizeMethod === 'crop') { + if (srcRatio > trgRatio) { + info.srcHeight = file.height; + info.srcWidth = info.srcHeight * trgRatio; + } else { + info.srcWidth = file.width; + info.srcHeight = info.srcWidth / trgRatio; + } + } else if (resizeMethod === 'contain') { + // Method 'contain' + if (srcRatio > trgRatio) { + height = width / srcRatio; + } else { + width = height * srcRatio; + } + } else { + throw new Error(`Unknown resizeMethod '${resizeMethod}'`); + } + } + + info.srcX = (file.width - info.srcWidth) / 2; + info.srcY = (file.height - info.srcHeight) / 2; + + info.trgWidth = width; + info.trgHeight = height; + + return info; + }, + + /** + * Can be used to transform the file (for example, resize an image if necessary). + * + * The default implementation uses `resizeWidth` and `resizeHeight` (if provided) and resizes + * images according to those dimensions. + * + * Gets the `file` as the first parameter, and a `done()` function as the second, that needs + * to be invoked with the file when the transformation is done. + */ + transformFile(file, done) { + if ((this.options.resizeWidth || this.options.resizeHeight) && file.type.match(/image.*/)) { + return this.resizeImage(file, this.options.resizeWidth, this.options.resizeHeight, this.options.resizeMethod, done); + } else { + return done(file); + } + }, + + + /** + * A string that contains the template used for each dropped + * file. Change it to fulfill your needs but make sure to properly + * provide all elements. + * + * If you want to use an actual HTML element instead of providing a String + * as a config option, you could create a div with the id `tpl`, + * put the template inside it and provide the element like this: + * + * document + * .querySelector('#tpl') + * .innerHTML + * + */ + previewTemplate: `\ +
+
+
+
+
+
+
+
+
+ + Check + + + + + +
+
+ + Error + + + + + + + +
+
\ + `, + + // END OPTIONS + // (Required by the dropzone documentation parser) + + + /* + 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) { + return this.element.classList.remove("dz-drag-hover"); + }, + dragstart(e) { + }, + dragend(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + dragenter(e) { + return this.element.classList.add("dz-drag-hover"); + }, + dragover(e) { + return this.element.classList.add("dz-drag-hover"); + }, + dragleave(e) { + return this.element.classList.remove("dz-drag-hover"); + }, + + paste(e) { + }, + + // Called whenever there are no files left in the dropzone anymore, and the + // dropzone should be displayed as if in the initial state. + reset() { + return this.element.classList.remove("dz-started"); + }, + + // Called when a file is added to the queue + // Receives `file` + addedfile(file) { + 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; // Backwards compatibility + + this.previewsContainer.appendChild(file.previewElement); + for (var node of file.previewElement.querySelectorAll("[data-dz-name]")) { + node.textContent = file.name; + } + for (node of file.previewElement.querySelectorAll("[data-dz-size]")) { + node.innerHTML = this.filesize(file.size); + } + + if (this.options.addRemoveLinks) { + file._removeLink = Dropzone.createElement(`${this.options.dictRemoveFile}`); + file.previewElement.appendChild(file._removeLink); + } + + let removeFileEvent = e => { + e.preventDefault(); + e.stopPropagation(); + if (file.status === Dropzone.UPLOADING) { + return Dropzone.confirm(this.options.dictCancelUploadConfirmation, () => this.removeFile(file)); + } else { + if (this.options.dictRemoveFileConfirmation) { + return Dropzone.confirm(this.options.dictRemoveFileConfirmation, () => this.removeFile(file)); + } else { + return this.removeFile(file); + } + } + }; + + for (let removeLink of file.previewElement.querySelectorAll("[data-dz-remove]")) { + removeLink.addEventListener("click", removeFileEvent); + } + } + }, + + + // Called whenever a file is removed. + removedfile(file) { + if (file.previewElement != null && file.previewElement.parentNode != null) { + file.previewElement.parentNode.removeChild(file.previewElement); + } + return this._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 (let thumbnailElement of file.previewElement.querySelectorAll("[data-dz-thumbnail]")) { + thumbnailElement.alt = file.name; + thumbnailElement.src = dataUrl; + } + + return 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"); + if ((typeof message !== "String") && message.error) { + message = message.error; + } + for (let node of file.previewElement.querySelectorAll("[data-dz-errormessage]")) { + node.textContent = message; + } + } + }, + + errormultiple() { + }, + + // 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"); + if (file._removeLink) { + return file._removeLink.textContent = this.options.dictCancelUpload; + } + } + }, + + processingmultiple() { + }, + + // 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 (let node of file.previewElement.querySelectorAll("[data-dz-uploadprogress]")) { + node.nodeName === 'PROGRESS' ? + (node.value = progress) + : + (node.style.width = `${progress}%`) + } + } + }, + + // Called whenever the total upload progress gets updated. + // Called with totalUploadProgress (0-100), totalBytes and totalBytesSent + totaluploadprogress() { + }, + + // 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() { + }, + + sendingmultiple() {}, + + // When the complete upload is finished and successful + // Receives `file` + success(file) { + if (file.previewElement) { + return file.previewElement.classList.add("dz-success"); + } + }, + + successmultiple() {}, + + // When the upload is canceled. + canceled(file) { + return this.emit("error", file, "Upload canceled."); + }, + + canceledmultiple() {}, + + // When the upload is finished, either with success or an error. + // Receives `file` + complete(file) { + if (file._removeLink) { + file._removeLink.textContent = this.options.dictRemoveFile; + } + if (file.previewElement) { + return file.previewElement.classList.add("dz-complete"); + } + }, + + completemultiple() {}, + + maxfilesexceeded() {}, + + maxfilesreached() {}, + + queuecomplete() {}, + + addedfiles() {} + }; + + + this.prototype._thumbnailQueue = []; + this.prototype._processingThumbnail = false; + } + + // global utility + static extend(target, ...objects) { + for (let object of objects) { + for (let key in object) { + let val = object[key]; + target[key] = val; + } + } + return target; + } + + constructor(el, options) { + super(); + let fallback, left; + this.element = el; + // For backwards compatibility since the version was in the prototype previously + this.version = Dropzone.version; + + this.defaultOptions.previewTemplate = this.defaultOptions.previewTemplate.replace(/\n*/g, ""); + + this.clickableElements = []; + this.listeners = []; + this.files = []; // All files + + if (typeof this.element === "string") { + this.element = document.querySelector(this.element); + } + + // Not checking if instance of HTMLElement or Element since IE9 is extremely weird. + if (!this.element || (this.element.nodeType == null)) { + throw new Error("Invalid dropzone element."); + } + + if (this.element.dropzone) { + throw new Error("Dropzone already attached."); + } + + // Now add this dropzone to the instances. + Dropzone.instances.push(this); + + // Put the dropzone inside the element itself. + this.element.dropzone = this; + + let elementOptions = (left = Dropzone.optionsForElement(this.element)) != null ? left : {}; + + this.options = Dropzone.extend({}, this.defaultOptions, elementOptions, options != null ? options : {}); + + // If the browser failed, just call the fallback and leave + if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { + return this.options.fallback.call(this); + } + + // @options.url = @element.getAttribute "action" unless @options.url? + 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."); + } + + // Backwards compatibility + if (this.options.acceptedMimeTypes) { + this.options.acceptedFiles = this.options.acceptedMimeTypes; + delete this.options.acceptedMimeTypes; + } + + // Backwards compatibility + if (this.options.renameFilename != null) { + this.options.renameFile = file => this.options.renameFilename.call(this, file.name, file); + } + + this.options.method = this.options.method.toUpperCase(); + + if ((fallback = this.getExistingFallback()) && 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 (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(); + } + + + // Returns all files that have been accepted + getAcceptedFiles() { + return this.files.filter((file) => file.accepted).map((file) => file); + } + + // Returns all files that have been rejected + // Not sure when that's going to be useful, but added for completeness. + getRejectedFiles() { + return this.files.filter((file) => !file.accepted).map((file) => file); + } + + getFilesWithStatus(status) { + return this.files.filter((file) => file.status === status).map((file) => file); + } + + // Returns all files that are in the queue + getQueuedFiles() { + return this.getFilesWithStatus(Dropzone.QUEUED); + } + + getUploadingFiles() { + return this.getFilesWithStatus(Dropzone.UPLOADING); + } + + getAddedFiles() { + return this.getFilesWithStatus(Dropzone.ADDED); + } + + // Files that are either queued or uploading + getActiveFiles() { + return this.files.filter((file) => (file.status === Dropzone.UPLOADING) || (file.status === Dropzone.QUEUED)).map((file) => file); + } + + // The function that gets called when Dropzone is initialized. You + // can (and should) setup event listeners inside this function. + init() { + // In case it isn't set already + 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) { + let setupHiddenFileInput = () => { + 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); + } + + // Not setting `display="none"` because some browsers don't accept clicks + // on elements that aren't displayed. + 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", () => { + let {files} = this.hiddenFileInput; + if (files.length) { + for (let file of files) { + this.addFile(file); + } + } + this.emit("addedfiles", files); + return setupHiddenFileInput(); + }); + }; + setupHiddenFileInput(); + } + + this.URL = window.URL !== null ? 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. + for (let eventName of this.events) { + this.on(eventName, this.options[eventName]); + } + + this.on("uploadprogress", () => this.updateTotalUploadProgress()); + + this.on("removedfile", () => this.updateTotalUploadProgress()); + + this.on("canceled", file => this.emit("complete", file)); + + // Emit a `queuecomplete` event if all files finished uploading. + this.on("complete", file => { + if ((this.getAddedFiles().length === 0) && (this.getUploadingFiles().length === 0) && (this.getQueuedFiles().length === 0)) { + // This needs to be deferred so that `queuecomplete` really triggers after `complete` + return setTimeout((() => this.emit("queuecomplete")), 0); + } + }); + + + let noPropagation = function (e) { + e.stopPropagation(); + if (e.preventDefault) { + return e.preventDefault(); + } else { + return e.returnValue = false; + } + }; + + // Create the listeners + this.listeners = [ + { + element: this.element, + events: { + "dragstart": e => { + return this.emit("dragstart", e); + }, + "dragenter": e => { + noPropagation(e); + return this.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) + let efct; + try { + efct = e.dataTransfer.effectAllowed; + } catch (error) { + } + e.dataTransfer.dropEffect = ('move' === efct) || ('linkMove' === efct) ? 'move' : 'copy'; + + noPropagation(e); + return this.emit("dragover", e); + }, + "dragleave": e => { + return this.emit("dragleave", e); + }, + "drop": e => { + noPropagation(e); + return this.drop(e); + }, + "dragend": e => { + return this.emit("dragend", e); + } + } + + // This is disabled right now, because the browsers don't implement it properly. + // "paste": (e) => + // noPropagation e + // @paste e + } + ]; + + this.clickableElements.forEach(clickableElement => { + return this.listeners.push({ + element: clickableElement, + events: { + "click": evt => { + // Only the actual dropzone or the message element should trigger file selection + if ((clickableElement !== this.element) || ((evt.target === this.element) || Dropzone.elementInside(evt.target, this.element.querySelector(".dz-message")))) { + this.hiddenFileInput.click(); // Forward the click + } + return true; + } + } + }); + }); + + this.enable(); + + return this.options.init.call(this); + } + + // Not fully tested yet + destroy() { + this.disable(); + this.removeAllFiles(true); + if (this.hiddenFileInput != null ? this.hiddenFileInput.parentNode : undefined) { + this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput); + this.hiddenFileInput = null; + } + delete this.element.dropzone; + return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); + } + + + updateTotalUploadProgress() { + let totalUploadProgress; + let totalBytesSent = 0; + let totalBytes = 0; + + let activeFiles = this.getActiveFiles(); + + if (activeFiles.length) { + for (let file of this.getActiveFiles()) { + totalBytesSent += file.upload.bytesSent; + totalBytes += file.upload.total; + } + totalUploadProgress = (100 * totalBytesSent) / totalBytes; + } else { + totalUploadProgress = 100; + } + + return this.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 this.options.paramName === "function") { + return this.options.paramName(n); + } else { + return `${this.options.paramName}${this.options.uploadMultiple ? `[${n}]` : ""}`; + } + } + + // If @options.renameFile is a function, + // the function will be used to rename the file.name before appending it to the formData + _renameFile(file) { + if (typeof this.options.renameFile !== "function") { + return file.name; + } + return this.options.renameFile(file); + } + + // 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() { + let existingFallback, form; + if (existingFallback = this.getExistingFallback()) { + return existingFallback; + } + + let fieldsString = "
"; + if (this.options.dictFallbackText) { + fieldsString += `

${this.options.dictFallbackText}

`; + } + fieldsString += `
`; + + let fields = Dropzone.createElement(fieldsString); + if (this.element.tagName !== "FORM") { + form = Dropzone.createElement(`
`); + form.appendChild(fields); + } else { + // Make sure that the enctype and method attributes are set properly + this.element.setAttribute("enctype", "multipart/form-data"); + this.element.setAttribute("method", this.options.method); + } + return form != null ? form : fields; + } + + + // Returns the fallback elements if they exist already + // + // This code has to pass in IE7 :( + getExistingFallback() { + let getFallback = function (elements) { + for (let el of elements) { + if (/(^| )fallback($| )/.test(el.className)) { + return el; + } + } + }; + + for (let tagName of ["div", "form"]) { + var fallback; + if (fallback = getFallback(this.element.getElementsByTagName(tagName))) { + return fallback; + } + } + } + + + // Activates all listeners stored in @listeners + setupEventListeners() { + return this.listeners.map((elementListeners) => + (() => { + let result = []; + for (let event in elementListeners.events) { + let listener = elementListeners.events[event]; + result.push(elementListeners.element.addEventListener(event, listener, false)); + } + return result; + })()); + } + + + // Deactivates all listeners stored in @listeners + removeEventListeners() { + return this.listeners.map((elementListeners) => + (() => { + let result = []; + for (let event in elementListeners.events) { + let listener = elementListeners.events[event]; + result.push(elementListeners.element.removeEventListener(event, listener, false)); + } + return result; + })()); + } + + // Removes all event listeners and cancels all files in the queue or being processed. + disable() { + this.clickableElements.forEach(element => element.classList.remove("dz-clickable")); + this.removeEventListeners(); + + return this.files.map((file) => this.cancelUpload(file)); + } + + enable() { + this.clickableElements.forEach(element => element.classList.add("dz-clickable")); + return this.setupEventListeners(); + } + + // Returns a nicely formatted filesize + filesize(size) { + let selectedSize = 0; + let selectedUnit = "b"; + + if (size > 0) { + let units = ['tb', 'gb', 'mb', 'kb', 'b']; + + for (let i = 0; i < units.length; i++) { + let unit = units[i]; + let 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; // Cutting of digits + } + + return `${selectedSize} ${this.options.dictFileSizeUnits[selectedUnit]}`; + } + + + // Adds or removes the `dz-max-files-reached` class from the form. + _updateMaxFilesReachedClass() { + 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"); + } + } + + + drop(e) { + if (!e.dataTransfer) { + return; + } + this.emit("drop", e); + + let {files} = e.dataTransfer; + this.emit("addedfiles", files); + + // Even if it's a folder, files.length will contain the folders. + if (files.length) { + let {items} = e.dataTransfer; + if (items && items.length && (items[0].webkitGetAsEntry != null)) { + // The browser supports dropping of folders, so handle items instead of files + this._addFilesFromItems(items); + } else { + this.handleFiles(files); + } + } + } + + paste(e) { + if (__guard__(e != null ? e.clipboardData : undefined, x => x.items) == null) { + return; + } + + this.emit("paste", e); + let {items} = e.clipboardData; + + if (items.length) { + return this._addFilesFromItems(items); + } + } + + + handleFiles(files) { + return files.map((file) => this.addFile(file)); + } + + // When a folder is dropped (or files are pasted), items must be handled + // instead of files. + _addFilesFromItems(items) { + return (() => { + let result = []; + for (let item of items) { + var entry; + if ((item.webkitGetAsEntry != null) && (entry = item.webkitGetAsEntry())) { + if (entry.isFile) { + result.push(this.addFile(item.getAsFile())); + } else if (entry.isDirectory) { + // Append all files from that directory to files + result.push(this._addFilesFromDirectory(entry, entry.name)); + } else { + result.push(undefined); + } + } else if (item.getAsFile != null) { + if ((item.kind == null) || (item.kind === "file")) { + result.push(this.addFile(item.getAsFile())); + } else { + result.push(undefined); + } + } else { + result.push(undefined); + } + } + return result; + })(); + } + + + // Goes through the directory, and adds each file it finds recursively + _addFilesFromDirectory(directory, path) { + let dirReader = directory.createReader(); + + let errorHandler = error => __guardMethod__(console, 'log', o => o.log(error)); + + var readEntries = () => { + return dirReader.readEntries(entries => { + if (entries.length > 0) { + for (let entry of entries) { + if (entry.isFile) { + entry.file(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}`); + } + } + + // 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); + }; + + return 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 > (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); + } + } + + 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, + filename: this._renameFile(file) + }; + this.files.push(file); + + file.status = Dropzone.ADDED; + + this.emit("addedfile", file); + + this._enqueueThumbnail(file); + + return this.accept(file, error => { + if (error) { + file.accepted = false; + this._errorProcessing([file], error); // Will set the file.status + } else { + file.accepted = true; + if (this.options.autoQueue) { + this.enqueueFile(file); + } // Will set .accepted = true + } + return this._updateMaxFilesReachedClass(); + }); + } + + + // Wrapper for enqueueFile + enqueueFiles(files) { + for (let file of files) { + this.enqueueFile(file); + } + return null; + } + + enqueueFile(file) { + if ((file.status === Dropzone.ADDED) && (file.accepted === true)) { + file.status = Dropzone.QUEUED; + if (this.options.autoProcessQueue) { + return setTimeout((() => this.processQueue()), 0); // Deferring the call + } + } else { + throw new Error("This file can't be queued because it has already been processed or was rejected."); + } + } + + _enqueueThumbnail(file) { + if (this.options.createImageThumbnails && file.type.match(/image.*/) && (file.size <= (this.options.maxThumbnailFilesize * 1024 * 1024))) { + this._thumbnailQueue.push(file); + return setTimeout((() => this._processThumbnailQueue()), 0); // Deferring the call + } + } + + _processThumbnailQueue() { + if (this._processingThumbnail || (this._thumbnailQueue.length === 0)) { + return; + } + + this._processingThumbnail = true; + let file = this._thumbnailQueue.shift(); + return this.createThumbnail(file, this.options.thumbnailWidth, this.options.thumbnailHeight, this.options.thumbnailMethod, true, dataUrl => { + this.emit("thumbnail", file, dataUrl); + this._processingThumbnail = false; + return this._processThumbnailQueue(); + }); + } + + + // Can be called by the user to remove a file + removeFile(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"); + } + } + + // Removes all files that aren't currently processed from the list + removeAllFiles(cancelIfNecessary) { + // Create a copy of files since removeFile() changes the @files array. + if (cancelIfNecessary == null) { + cancelIfNecessary = false; + } + for (let file of this.files.slice()) { + if ((file.status !== Dropzone.UPLOADING) || cancelIfNecessary) { + this.removeFile(file); + } + } + return null; + } + + // Resizes an image before it gets sent to the server. This function is the default behavior of + // `options.transformFile` if `resizeWidth` or `resizeHeight` are set. The callback is invoked with + // the resized blob. + resizeImage(file, width, height, resizeMethod, callback) { + return this.createThumbnail(file, width, height, resizeMethod, false, (dataUrl, canvas) => { + if (canvas === null) { + // The image has not been resized + return callback(file); + } else { + let {resizeMimeType} = this.options; + if (resizeMimeType == null) { + resizeMimeType = file.type; + } + let resizedDataURL = canvas.toDataURL(resizeMimeType, this.options.resizeQuality); + if ((resizeMimeType === 'image/jpeg') || (resizeMimeType === 'image/jpg')) { + // Now add the original EXIF information + resizedDataURL = ExifRestore.restore(file.dataURL, resizedDataURL); + } + return callback(Dropzone.dataURItoBlob(resizedDataURL)); + } + }); + } + + createThumbnail(file, width, height, resizeMethod, fixOrientation, callback) { + let fileReader = new FileReader; + + fileReader.onload = () => { + + file.dataURL = fileReader.result; + + // Don't bother creating a thumbnail for SVG images since they're vector + if (file.type === "image/svg+xml") { + if (callback != null) { + callback(fileReader.result); + } + return; + } + + return this.createThumbnailFromUrl(file, width, height, resizeMethod, fixOrientation, callback); + }; + + return fileReader.readAsDataURL(file); + } + + createThumbnailFromUrl(file, width, height, resizeMethod, fixOrientation, callback, crossOrigin) { + // Not using `new Image` here because of a bug in latest Chrome versions. + // See https://github.com/enyo/dropzone/pull/226 + let img = document.createElement("img"); + + if (crossOrigin) { + img.crossOrigin = crossOrigin; + } + + img.onload = () => { + let loadExif = callback => callback(1); + if ((typeof EXIF !== 'undefined' && EXIF !== null) && fixOrientation) { + loadExif = callback => + EXIF.getData(img, function () { + return callback(EXIF.getTag(this, 'Orientation')); + }) + ; + } + + return loadExif(orientation => { + file.width = img.width; + file.height = img.height; + + let resizeInfo = this.options.resize.call(this, file, width, height, resizeMethod); + + let canvas = document.createElement("canvas"); + let ctx = canvas.getContext("2d"); + + canvas.width = resizeInfo.trgWidth; + canvas.height = resizeInfo.trgHeight; + + if (orientation > 4) { + canvas.width = resizeInfo.trgHeight; + canvas.height = resizeInfo.trgWidth; + } + + switch (orientation) { + case 2: + // horizontal flip + ctx.translate(canvas.width, 0); + ctx.scale(-1, 1); + break; + case 3: + // 180° rotate left + ctx.translate(canvas.width, canvas.height); + ctx.rotate(Math.PI); + break; + case 4: + // vertical flip + ctx.translate(0, canvas.height); + ctx.scale(1, -1); + break; + case 5: + // vertical flip + 90 rotate right + ctx.rotate(0.5 * Math.PI); + ctx.scale(1, -1); + break; + case 6: + // 90° rotate right + ctx.rotate(0.5 * Math.PI); + ctx.translate(0, -canvas.height); + break; + case 7: + // horizontal flip + 90 rotate right + ctx.rotate(0.5 * Math.PI); + ctx.translate(canvas.width, -canvas.height); + ctx.scale(-1, 1); + break; + case 8: + // 90° rotate left + ctx.rotate(-0.5 * Math.PI); + ctx.translate(-canvas.width, 0); + break; + } + + // This is a bugfix for iOS' scaling bug. + drawImageIOSFix(ctx, img, resizeInfo.srcX != null ? resizeInfo.srcX : 0, resizeInfo.srcY != null ? resizeInfo.srcY : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, resizeInfo.trgX != null ? resizeInfo.trgX : 0, resizeInfo.trgY != null ? resizeInfo.trgY : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); + + let thumbnail = canvas.toDataURL("image/png"); + + if (callback != null) { + return callback(thumbnail, canvas); + } + }); + }; + + if (callback != null) { + img.onerror = callback; + } + + return img.src = file.dataURL; + } + + + // Goes through the queue and processes files if there aren't too many already. + processQueue() { + let {parallelUploads} = this.options; + let processingLength = this.getUploadingFiles().length; + let i = processingLength; + + // There are already at least as many files uploading than should be + if (processingLength >= parallelUploads) { + return; + } + + let queuedFiles = this.getQueuedFiles(); + + if (!(queuedFiles.length > 0)) { + return; + } + + if (this.options.uploadMultiple) { + // The files should be uploaded in one request + return this.processFiles(queuedFiles.slice(0, (parallelUploads - processingLength))); + } else { + while (i < parallelUploads) { + if (!queuedFiles.length) { + return; + } // Nothing left to process + this.processFile(queuedFiles.shift()); + i++; + } + } + } + + + // Wrapper for `processFiles` + processFile(file) { + return this.processFiles([file]); + } + + + // Loads the file, then calls finishedLoading() + processFiles(files) { + for (let file of files) { + file.processing = true; // Backwards compatibility + file.status = Dropzone.UPLOADING; + + this.emit("processing", file); + } + + if (this.options.uploadMultiple) { + this.emit("processingmultiple", files); + } + + return this.uploadFiles(files); + } + + + _getFilesWithXhr(xhr) { + let files; + return files = (this.files.filter((file) => file.xhr === xhr).map((file) => file)); + } + + + // 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) { + let groupedFiles = this._getFilesWithXhr(file.xhr); + for (var groupedFile of groupedFiles) { + groupedFile.status = Dropzone.CANCELED; + } + file.xhr.abort(); + for (groupedFile of groupedFiles) { + this.emit("canceled", groupedFile); + } + if (this.options.uploadMultiple) { + this.emit("canceledmultiple", groupedFiles); + } + + } else if (file.status === Dropzone.ADDED || file.status === 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(option, ...args) { + if (typeof option === 'function') { + return option.apply(this, args); + } + return option; + } + + // Wrapper for uploadFiles() + uploadFile(file) { + return this.uploadFiles([file]); + } + + uploadFiles(files) { + let result, value; + let xhr = new XMLHttpRequest(); + + // Put the xhr object in the file objects to be able to reference it later. + for (var file of files) { + file.xhr = xhr; + } + + let method = this.resolveOption(this.options.method, files); + let url = this.resolveOption(this.options.url, files); + xhr.open(method, url, true); + + // Setting the timeout after open because of IE11 issue: https://gitlab.com/meno/dropzone/issues/8 + xhr.timeout = this.resolveOption(this.options.timeout, files); + + // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179 + xhr.withCredentials = !!this.options.withCredentials; + + + let response = null; + + let handleError = () => { + return (() => { + result = []; + for (file of files) { + result.push(this._errorProcessing(files, response || this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr)); + } + return result; + })(); + }; + + + let updateProgress = e => { + let progress; + if (e != null) { + progress = (100 * e.loaded) / e.total; + + for (file of files) { + file.upload.progress = progress; + file.upload.total = e.total; + file.upload.bytesSent = e.loaded; + } + } else { + // Called when the file finished uploading + + let allFilesFinished = true; + + progress = 100; + + for (file of files) { + if ((file.upload.progress !== 100) || (file.upload.bytesSent !== file.upload.total)) { + allFilesFinished = false; + } + file.upload.progress = progress; + file.upload.bytesSent = file.upload.total; + } + + // Nothing to do, all files already at 100% + if (allFilesFinished) { + return; + } + } + + return (() => { + result = []; + for (file of files) { + result.push(this.emit("uploadprogress", file, progress, file.upload.bytesSent)); + } + return result; + })(); + }; + + xhr.onload = e => { + if (files[0].status === Dropzone.CANCELED) { + return; + } + + if (xhr.readyState !== 4) { + return; + } + + if ((xhr.responseType !== 'arraybuffer') && (xhr.responseType !== 'blob')) { + 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 <= xhr.status && xhr.status < 300)) { + return handleError(); + } else { + return this._finished(files, response, e); + } + }; + + xhr.onerror = () => { + if (files[0].status === Dropzone.CANCELED) { + return; + } + return handleError(); + }; + + // Some browsers do not have the .upload property + let progressObj = xhr.upload != null ? xhr.upload : xhr; + progressObj.onprogress = updateProgress; + + let headers = { + "Accept": "application/json", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest", + }; + + if (this.options.headers) { + Dropzone.extend(headers, this.options.headers); + } + + for (let headerName in headers) { + let headerValue = headers[headerName]; + if (headerValue) { + xhr.setRequestHeader(headerName, headerValue); + } + } + + let formData = new FormData(); + + // Adding all @options parameters + if (this.options.params) { + for (let key in this.options.params) { + value = this.options.params[key]; + formData.append(key, value); + } + } + + // Let the user add additional data if necessary + for (file of files) { + this.emit("sending", file, xhr, formData); + } + if (this.options.uploadMultiple) { + this.emit("sendingmultiple", files, xhr, formData); + } + + + // Take care of other input elements + if (this.element.tagName === "FORM") { + for (let input of this.element.querySelectorAll("input, textarea, select, button")) { + let inputName = input.getAttribute("name"); + let inputType = input.getAttribute("type"); + if (inputType) inputType = inputType.toLowerCase(); + + // If the input doesn't have a name, we can't use it. + if (inputName == null) continue; + + if ((input.tagName === "SELECT") && input.hasAttribute("multiple")) { + // Possibly multiple values + for (let option of input.options) { + if (option.selected) { + formData.append(inputName, option.value); + } + } + } else if (!inputType || (inputType !== "checkbox" && inputType !== "radio") || input.checked) { + formData.append(inputName, input.value); + } + } + } + + + // Finally add the files + // Has to be last because some servers (eg: S3) expect the file to be the last parameter + + // Clumsy way of handling asynchronous calls, until I get to add a proper Future library. + let doneCounter = 0; + + return (() => { + result = []; + for (let i = 0, end = files.length - 1, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { + let doneFunction = (file, paramName, fileName) => transformedFile => { + formData.append(paramName, transformedFile, fileName); + if (++doneCounter === files.length) { + return this.submitRequest(xhr, formData, files); + } + }; + + result.push(this.options.transformFile.call(this, files[i], doneFunction(files[i], this._getParamName(i), files[i].upload.filename))); + } + return result; + })(); + } + + + submitRequest(xhr, formData, files) { + return xhr.send(formData); + } + + // Called internally when processing is finished. + // Individual callbacks have to be called in the appropriate sections. + _finished(files, responseText, e) { + for (let file of files) { + 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(); + } + } + + // Called internally when processing is finished. + // Individual callbacks have to be called in the appropriate sections. + _errorProcessing(files, message, xhr) { + for (let file of files) { + 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(); + } + } + } + Dropzone.initClass(); + + + Dropzone.version = "5.2.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 = {}; - Dropzone.optionsForElement = function(element) { - if (element.getAttribute("id")) { - return Dropzone.options[camelize(element.getAttribute("id"))]; - } else { - return void 0; - } + + // Returns the options for an element or undefined if none available. + Dropzone.optionsForElement = function (element) { + // Get the `Dropzone.options.elementId` for this element if it exists + if (element.getAttribute("id")) { + return Dropzone.options[camelize(element.getAttribute("id"))]; + } else { + return undefined; + } }; + + // Holds a list of all dropzone instances 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; + // Returns the dropzone for given element if any + Dropzone.forElement = function (element) { + if (typeof element === "string") { + element = document.querySelector(element); + } + if ((element != null ? element.dropzone : undefined) == 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; }; + + // Set to false if you don't want Dropzone to automatically find and attach to .dropzone elements. 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; + // Looks for all .dropzone elements and creates a dropzone for them + Dropzone.discover = function () { + let dropzones; + if (document.querySelectorAll) { + dropzones = document.querySelectorAll(".dropzone"); + } else { + dropzones = []; + // IE :( + let checkElements = elements => + (() => { + let result = []; + for (let el of elements) { + if (/(^| )dropzone($| )/.test(el.className)) { + result.push(dropzones.push(el)); + } else { + result.push(undefined); + } + } + return result; + })() + ; + checkElements(document.getElementsByTagName("div")); + checkElements(document.getElementsByTagName("form")); + } + + return (() => { + let result = []; + for (let dropzone of dropzones) { + // Create a dropzone unless auto discover has been disabled for specific element + if (Dropzone.optionsForElement(dropzone) !== false) { + result.push(new Dropzone(dropzone)); + } else { + result.push(undefined); + } + } + return result; + })(); }; - 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; + // 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 and windows phone version of opera 12 seems to have a problem with the File drag'n'drop API. + /opera.*(Macintosh|Windows Phone).*version\/12/i + ]; + + + // Checks if the browser is supported + Dropzone.isBrowserSupported = function () { + let capableBrowser = true; + + if (window.File && window.FileReader && window.FileList && window.Blob && window.FormData && document.querySelector) { + if (!("classList" in document.createElement("a"))) { + capableBrowser = false; + } else { + // The browser supports the API, but may be blacklisted. + for (let regex of Dropzone.blacklistedBrowsers) { + 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; + Dropzone.dataURItoBlob = function (dataURI) { + // convert base64 to raw binary data held in a string + // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this + let byteString = atob(dataURI.split(',')[1]); + + // separate out the mime component + let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + // write the bytes of the string to an ArrayBuffer + let ab = new ArrayBuffer(byteString.length); + let ia = new Uint8Array(ab); + for (let i = 0, end = byteString.length, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { + ia[i] = byteString.charCodeAt(i); + } + + // write the ArrayBuffer to a blob + return new Blob([ab], {type: mimeString}); }; - camelize = function(str) { - return str.replace(/[\-_](\w)/g, function(match) { - return match.charAt(1).toUpperCase(); - }); + // Returns an array without the rejected item + const without = (list, rejectedItem) => list.filter((item) => item !== rejectedItem).map((item) => item); + + // abc-def_ghi -> abcDefGhi + const camelize = str => str.replace(/[\-_](\w)/g, match => match.charAt(1).toUpperCase()); + + // Creates an element from string + Dropzone.createElement = function (string) { + let div = document.createElement("div"); + div.innerHTML = string; + return div.childNodes[0]; }; - Dropzone.createElement = function(string) { - var div; - div = document.createElement("div"); - div.innerHTML = string; - return div.childNodes[0]; + // Tests if given element is inside (or simply is) the container + Dropzone.elementInside = function (element, container) { + if (element === container) { + return true; + } // Coffeescript doesn't support do/while loops + while ((element = element.parentNode)) { + if (element === container) { + return true; + } + } + return false; }; - 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) { + let 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.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) { + let el, elements; + if (els instanceof Array) { + elements = []; + try { + for (el of els) { + elements.push(this.getElement(el, name)); + } + } catch (e) { + elements = null; + } + } else if (typeof els === "string") { + elements = []; + for (el of document.querySelectorAll(els)) { + 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.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; + // 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 = function (question, accepted, rejected) { + if (window.confirm(question)) { + return accepted(); + } else if (rejected != null) { + return rejected(); + } }; - Dropzone.confirm = function(question, accepted, rejected) { - if (window.confirm(question)) { - return accepted(); - } else if (rejected != null) { - return rejected(); - } + // Validates the mime type like this: + // + // https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept + Dropzone.isValidFile = function (file, acceptedFiles) { + if (!acceptedFiles) { + return true; + } // If there are no accepted mime types, it's OK + acceptedFiles = acceptedFiles.split(","); + + let mimeType = file.type; + let baseMimeType = mimeType.replace(/\/.*$/, ""); + + for (let validType of acceptedFiles) { + 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)) { + // This is something like a image/* mime type + if (baseMimeType === validType.replace(/\/.*$/, "")) { + return true; + } + } else { + if (mimeType === validType) { + return true; + } + } + } + + return false; }; - 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); - }); - }; + // Augment jQuery + 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; + + if (typeof module !== 'undefined' && module !== null) { + module.exports = Dropzone; } else { - window.Dropzone = Dropzone; + 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; + 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 + + 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; - } + // 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 + let detectVerticalSquash = function (img) { + let iw = img.naturalWidth; + let ih = img.naturalHeight; + let canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = ih; + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + let {data} = ctx.getImageData(1, 0, 1, ih); + + + // search image edge pixel position in case it is squashed vertically. + let sy = 0; + let ey = ih; + let py = ih; + while (py > sy) { + let alpha = data[((py - 1) * 4) + 3]; + + if (alpha === 0) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + let 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); + // A replacement for context.drawImage + // (args are for source and destination). + var drawImageIOSFix = function (ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) { + let vertSquashRatio = detectVerticalSquash(img); + return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); }; + // Based on MinifyJpeg + // Source: http://www.perry.cz/files/ExifRestorer.js + // http://elicon.blog57.fc2.com/blog-entry-206.html + class ExifRestore { + static initClass() { + this.KEY_STR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + } + + static encode64(input) { + let output = ''; + let chr1 = undefined; + let chr2 = undefined; + let chr3 = ''; + let enc1 = undefined; + let enc2 = undefined; + let enc3 = undefined; + let enc4 = ''; + let i = 0; + while (true) { + chr1 = input[i++]; + chr2 = input[i++]; + chr3 = input[i++]; + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + if (isNaN(chr2)) { + enc3 = (enc4 = 64); + } else if (isNaN(chr3)) { + enc4 = 64; + } + output = output + this.KEY_STR.charAt(enc1) + this.KEY_STR.charAt(enc2) + this.KEY_STR.charAt(enc3) + this.KEY_STR.charAt(enc4); + chr1 = (chr2 = (chr3 = '')); + enc1 = (enc2 = (enc3 = (enc4 = ''))); + if (!(i < input.length)) { + break; + } + } + return output; + } + + static restore(origFileBase64, resizedFileBase64) { + if (!origFileBase64.match('data:image/jpeg;base64,')) { + return resizedFileBase64; + } + let rawImage = this.decode64(origFileBase64.replace('data:image/jpeg;base64,', '')); + let segments = this.slice2Segments(rawImage); + let image = this.exifManipulation(resizedFileBase64, segments); + return `data:image/jpeg;base64,${this.encode64(image)}`; + } + + static exifManipulation(resizedFileBase64, segments) { + let exifArray = this.getExifArray(segments); + let newImageArray = this.insertExif(resizedFileBase64, exifArray); + let aBuffer = new Uint8Array(newImageArray); + return aBuffer; + } + + static getExifArray(segments) { + let seg = undefined; + let x = 0; + while (x < segments.length) { + seg = segments[x]; + if ((seg[0] === 255) & (seg[1] === 225)) { + return seg; + } + x++; + } + return []; + } + + static insertExif(resizedFileBase64, exifArray) { + let imageData = resizedFileBase64.replace('data:image/jpeg;base64,', ''); + let buf = this.decode64(imageData); + let separatePoint = buf.indexOf(255, 3); + let mae = buf.slice(0, separatePoint); + let ato = buf.slice(separatePoint); + let array = mae; + array = array.concat(exifArray); + array = array.concat(ato); + return array; + } + + static slice2Segments(rawImageArray) { + let head = 0; + let segments = []; + while (true) { + var length; + if ((rawImageArray[head] === 255) & (rawImageArray[head + 1] === 218)) { + break; + } + if ((rawImageArray[head] === 255) & (rawImageArray[head + 1] === 216)) { + head += 2; + } else { + length = (rawImageArray[head + 2] * 256) + rawImageArray[head + 3]; + let endPoint = head + length + 2; + let seg = rawImageArray.slice(head, endPoint); + segments.push(seg); + head = endPoint; + } + if (head > rawImageArray.length) { + break; + } + } + return segments; + } + + static decode64(input) { + let output = ''; + let chr1 = undefined; + let chr2 = undefined; + let chr3 = ''; + let enc1 = undefined; + let enc2 = undefined; + let enc3 = undefined; + let enc4 = ''; + let i = 0; + let buf = []; + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + let base64test = /[^A-Za-z0-9\+\/\=]/g; + if (base64test.exec(input)) { + console.warn('There were invalid base64 characters in the input text.\nValid base64 characters are A-Z, a-z, 0-9, \'+\', \'/\',and \'=\'\nExpect errors in decoding.'); + } + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); + while (true) { + enc1 = this.KEY_STR.indexOf(input.charAt(i++)); + enc2 = this.KEY_STR.indexOf(input.charAt(i++)); + enc3 = this.KEY_STR.indexOf(input.charAt(i++)); + enc4 = this.KEY_STR.indexOf(input.charAt(i++)); + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + buf.push(chr1); + if (enc3 !== 64) { + buf.push(chr2); + } + if (enc4 !== 64) { + buf.push(chr3); + } + chr1 = (chr2 = (chr3 = '')); + enc1 = (enc2 = (enc3 = (enc4 = ''))); + if (!(i < input.length)) { + break; + } + } + return buf; + } + } + ExifRestore.initClass(); + + /* * contentloaded.js * @@ -1712,56 +2541,68 @@ * 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); - } + // @win window reference + // @fn function reference + let contentLoaded = function (win, fn) { + let done = false; + let top = true; + let doc = win.document; + let root = doc.documentElement; + let add = (doc.addEventListener ? "addEventListener" : "attachEvent"); + let rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); + let pre = (doc.addEventListener ? "" : "on"); + var 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); + } + }; + + var poll = function () { + try { + root.doScroll("left"); + } catch (e) { + 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(); - } - }; + // As a single function to be able to write tests. + Dropzone._autoDiscoverFunction = function () { + if (Dropzone.autoDiscover) { + return Dropzone.discover(); + } + }; contentLoaded(window, Dropzone._autoDiscoverFunction); -}).call(this); + function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; + } + function __guardMethod__(obj, methodName, transform) { + if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { + return transform(obj, methodName); + } else { + return undefined; + } + }