diff --git a/src/pat/contentbrowser/src/ContentBrowser.svelte b/src/pat/contentbrowser/src/ContentBrowser.svelte index e39048f75..59e57a398 100644 --- a/src/pat/contentbrowser/src/ContentBrowser.svelte +++ b/src/pat/contentbrowser/src/ContentBrowser.svelte @@ -5,7 +5,7 @@ import { fly } from "svelte/transition"; import _t from "../../../core/i18n-wrapper"; import { ensureIntlSupport } from "../../../core/intl-loader"; - import Upload from "../../upload/upload"; + import Upload from "../../upload/upload--implementation"; import contentStore from "./ContentStore"; import { clickOutside, diff --git a/src/pat/relateditems/relateditems.js b/src/pat/relateditems/relateditems.js index dad490755..b26f594ba 100644 --- a/src/pat/relateditems/relateditems.js +++ b/src/pat/relateditems/relateditems.js @@ -401,7 +401,7 @@ export default Base.extend({ }, async initUploadView(disabled) { - let Upload = await import("../upload/upload"); + let Upload = await import("../upload/upload--implementation"); Upload = Upload.default; const upload_button = this.$toolbar[0].querySelector(".upload button"); diff --git a/src/pat/structure/js/views/upload.js b/src/pat/structure/js/views/upload.js index 31a672765..6e511bc31 100644 --- a/src/pat/structure/js/views/upload.js +++ b/src/pat/structure/js/views/upload.js @@ -1,7 +1,7 @@ import $ from "jquery"; import _ from "underscore"; import PopoverView from "../../../../core/ui/views/popover"; -import Upload from "../../../upload/upload"; +import Upload from "../../../upload/upload--implementation"; export default PopoverView.extend({ className: "popover upload", diff --git a/src/pat/upload/upload--implementation.js b/src/pat/upload/upload--implementation.js new file mode 100644 index 000000000..e759a1784 --- /dev/null +++ b/src/pat/upload/upload--implementation.js @@ -0,0 +1,508 @@ +import $ from "jquery"; +import _ from "underscore"; +import _t from "../../core/i18n-wrapper"; +import Base from "@patternslib/patternslib/src/core/base"; +import logger from "@patternslib/patternslib/src/core/logging"; +import utils from "../../core/utils"; + +const log = logger.getLogger("pat-upload"); + +let Dropzone; + +// The full pat-upload implementation. It stays out of the eager patterns chunk: +// the thin upload.js registration module loads it on first `.pat-upload` match +// and grafts its prototype. Imperative callers (pat-structure, pat-relateditems, +// the contentbrowser app) import this module directly and `new Upload(...)` it, +// so it must be a constructable Base.extend pattern, not a plain object grafted +// asynchronously. +export default Base.extend({ + name: "upload", + trigger: ".pat-upload", + parser: "mockup", + defaults: { + showTitle: true, + url: null, // XXX MUST provide url to submit to OR be in a form + className: "upload", + wrap: false, + wrapperTemplate: '
', + fileaddedClassName: "dropping", + useTus: false, + container: "", + ajaxUpload: true, + + paramName: "file", + addRemoveLinks: false, + autoCleanResults: true, + previewsContainer: ".previews", + previewTemplate: null, + maxFiles: null, + maxFilesize: 99999999, // let's not have a max by default... + + allowPathSelection: undefined, + relatedItems: { + // UID attribute is required here since we're working with related items + attributes: [ + "UID", + "Title", + "Description", + "getURL", + "portal_type", + "path", + "ModificationDate", + ], + batchSize: 20, + basePath: "/", + vocabularyUrl: null, + width: 500, + maximumSelectionSize: 1, + selectableTypes: ["Folder"], + }, + }, + + init: async function () { + import("dropzone/dist/dropzone.css"); + import("./upload.scss"); + Dropzone = (await import("dropzone")).default; + /* we do not want this plugin to auto discover */ + Dropzone.autoDiscover = false; + + const UploadTemplate = (await import("./templates/upload.xml")).default; + + var self = this; + + if (self.$el.find(".upload-area").length) { + log.info("Found already a dropzone on element, skipping!"); + return; + } + + if (typeof self.options.allowPathSelection === "undefined") { + // Set allowPathSelection to true, if we can use path based urls. + self.options.allowPathSelection = + self.options.baseUrl && self.options.relativePath; + } + + // TODO: find a way to make this work in firefox (and IE) + $(document).on("paste", function (e) { + var oe = e.originalEvent; + var items = oe.clipboardData.items; + if (items) { + for (var i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + var blob = items[i].getAsFile(); + self.dropzone.addFile(blob); + } + } + } + }); + // values that will change current processing + self.currentPath = self.options.currentPath; + self.currentFile = 0; + + let template = _.template(UploadTemplate)({ + _t: _t, + allowPathSelection: self.options.allowPathSelection, + }); + self.$el.addClass(self.options.className); + self.$el.append(template); + + self.$progress = $(".progress-bar-success", self.$el); + + if (!self.options.showTitle) { + self.$el.find("h2.title").hide(); + } + + if (!self.options.ajaxUpload) { + // no ajax upload, drop the fallback + $(".fallback", this.$el).remove(); + if (this.$el.hasClass(".upload-container")) { + this.$el.addClass("no-ajax-upload"); + } else { + this.$el.closest(".upload-container").addClass("no-ajax-upload"); + } + } + + if (self.options.wrap) { + self.$el.wrap(self.options.wrapperTemplate); + self.$el = self.$el.parent(); + } + + if (self.options.allowPathSelection) { + // only use related items if we can generate path based urls and if it's not turned off. + self.$pathInput = $('input[name="location"]', self.$el); + self.relatedItems = await self.setupRelatedItems(self.$pathInput); + } else { + $('input[name="location"]', self.$el).parent().remove(); + self.relatedItems = null; + } + + self.$dropzone = $(".upload-area", self.$el); + + $("div.browse-select button.browse", self.$el).on("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + if ( + !self.options.maxFiles || + self.dropzone.files.length < self.options.maxFiles + ) { + self.dropzone.hiddenFileInput.click(); + } + }); + + var dzoneOptions = await this.getDzoneOptions(); + + try { + // if init of Dropzone fails it says nothing and + // it fails silently. Using this block we make sure + // that if you break it w/ some weird or missing option + // you can get a proper log of it + // + self.dropzone = new Dropzone(self.$dropzone[0], dzoneOptions); + } catch (e) { + if (window.DEBUG) { + // log it! + console.log(e); + } + throw e; + } + + self.dropzone.on("maxfilesreached", function () { + self.showHideControls(); + }); + + self.dropzone.on("addedfile", function (/* file */) { + self.showHideControls(); + }); + + self.dropzone.on("removedfile", function () { + self.showHideControls(); + }); + + self.dropzone.on("success", function (e, response) { + // Trigger event 'uploadAllCompleted' and pass the server's reponse and + // the path uid. This event can be listened to by patterns using the + // upload pattern, e.g. the TinyMCE pattern's link plugin. + var data; + try { + data = $.parseJSON(response); + } catch { + data = response; + } + self.$el.trigger("uploadAllCompleted", { + data: data, + path_uid: self.$pathInput ? self.$pathInput.val() : null, + }); + }); + + if (self.options.autoCleanResults) { + self.dropzone.on("complete", function (file) { + if (file.status === Dropzone.SUCCESS) { + setTimeout(function () { + $(file.previewElement).fadeOut(); + }, 3000); + } + }); + } + + self.dropzone.on("complete", function (file) { + if (file.status === Dropzone.SUCCESS && self.dropzone.files.length === 1) { + self.showHideControls(); + } + }); + + self.dropzone.on("error", function (file, response, xmlhr) { + if (typeof xmlhr !== "undefined" && xmlhr.status !== 403) { + // If error other than 403, just print a generic message + $(".dz-error-message span", file.previewElement).html( + _t("The file transfer failed") + ); + } + }); + + self.dropzone.on("totaluploadprogress", function (pct) { + // need to caclulate total pct here in reality since we're manually + // processing each file one at a time. + pct = + (((self.currentFile - 1) * 100 + pct) / + (self.dropzone.files.length * 100)) * + 100; + self.$progress.attr("aria-valuenow", pct).css("width", pct + "%"); + }); + + $(".upload-all", self.$el).on("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + self.processUpload({ + finished: function () { + self.$progress.attr("aria-valuenow", 0).css("width", "0%"); + }, + }); + }); + if (self.options.clipboardfile) { + self.dropzone.addFile(self.options.clipboardfile); + } + }, + + showHideControls: function () { + /* we do this delayed because this can be called multiple times + AND we need to do this hide/show AFTER dropzone is done with + all it's own events. This is NASTY but the only way we can + enforce some numFiles with dropzone! */ + var self = this; + if (self._showHideTimeout) { + clearTimeout(self._showHideTimeout); + } + self._showHideTimeout = setTimeout(function () { + self._showHideControls(); + }, 50); + }, + + _showHideControls: function () { + var self = this; + var $controls = $(".controls", self.$el); + var $browse = $(".browse-select", self.$el); + var $input = $(".dz-hidden-input"); + + if (self.options.maxFiles) { + if (self.dropzone.files.length < self.options.maxFiles) { + $browse.show(); + $input.prop("disabled", false); + } else { + $browse.hide(); + $input.prop("disabled", true); + } + } + if (self.dropzone.files.length > 0) { + $controls.fadeIn("slow"); + var file = self.dropzone.files[0]; + $(".dz-error-message span", file.previewElement).html(""); + } else { + $controls.fadeOut("slow"); + } + }, + + pathJoin: function () { + var parts = []; + _.each(arguments, function (part) { + if (!part) { + return; + } + if (part[0] === "/") { + part = part.substring(1); + } + if (part[part.length - 1] === "/") { + part = part.substring(0, part.length - 1); + } + parts.push(part); + }); + return parts.join("/"); + }, + + getUrl: function () { + var self = this; + var url = self.options.url; + if (!url) { + if (self.options.baseUrl && self.options.relativePath) { + url = self.options.baseUrl; + if (url[url.length - 1] !== "/") { + url = url + "/"; + } + url = url + self.pathJoin(self.currentPath, self.options.relativePath); + } else { + var $form = self.$el.parents("form"); + if ($form.length > 0) { + url = $form.attr("action"); + } else { + url = window.location.href; + } + } + } + return url; + }, + + getDzoneOptions: async function () { + let PreviewTemplate = await import("./templates/preview.xml"); + PreviewTemplate = PreviewTemplate.default; + + var self = this; + + // This pattern REQUIRE dropzone to be clickable + self.options.clickable = true; + + var options = $.extend({}, self.options); + options.url = self.getUrl(); + + options.headers = { + "X-CSRF-TOKEN": utils.getAuthenticator(), + }; + + // XXX force to only upload one to the server at a time, + // right now we don't support multiple for backends + options.uploadMultiple = false; + + delete options.wrap; + delete options.wrapperTemplate; + delete options.resultTemplate; + delete options.autoCleanResults; + delete options.fileaddedClassName; + delete options.useTus; + + if (self.options.previewsContainer) { + /* + * if they have a select but it's not an id, let's make an id selector + * so we can target the correct container. dropzone is weird here... + */ + var $preview = self.$el.find(self.options.previewsContainer); + if ($preview.length > 0) { + options.previewsContainer = $preview[0]; + } + } + + // XXX: do we need to allow this? + options.autoProcessQueue = false; + // options.addRemoveLinks = true; // we show them in the template + options.previewTemplate = PreviewTemplate; + + // if our element is a form we should force some values + // https://github.com/enyo/dropzone/wiki/Combine-normal-form-with-Dropzone + return options; + }, + + processUpload: function (options) { + if (!options) { + options = {}; + } + + var self = this, + processing = false, + useTus = self.options.useTus, + fileaddedClassName = self.options.fileaddedClassName, + finished = options.finished; + + self.currentFile = 0; + + function process() { + processing = true; + if (self.dropzone.files.length === 0) { + processing = false; + } + + var file = self.dropzone.files[0]; + if (processing && file.status === Dropzone.ERROR) { + // Put the file back as "queued" for retrying + file.status = Dropzone.QUEUED; + processing = false; + } + + if (!processing) { + self.$el.removeClass(fileaddedClassName); + if (finished !== undefined && typeof finished === "function") { + finished(); + } + return; + } + + if ([Dropzone.SUCCESS, Dropzone.CANCELED].indexOf(file.status) !== -1) { + // remove it + self.dropzone.removeFile(file); + process(); + } else if (file.status !== Dropzone.UPLOADING) { + // start processing file + if (useTus && window.tus) { + // use tus upload if installed + self.handleTusUpload(file); + } else { + // otherwise, just use dropzone to process + self.currentFile += 1; + self.dropzone.processFile(file); + } + setTimeout(process, 100); + } else { + // currently processing + setTimeout(process, 100); + } + } + process(); + }, + + handleTusUpload: function (file) { + /* this needs fixing... */ + var self = this, + chunkSize = 1024 * 1024 * 5; // 5mb chunk size + + file.status = Dropzone.UPLOADING; + + window.tus + .upload(file, { + endpoint: self.dropzone.options.url, + headers: { + "FILENAME": file.name, + "X-CSRF-TOKEN": utils.getAuthenticator(), + }, + chunkSize: chunkSize, + }) + .fail(function () { + if (window.DEBUG) { + console.alert(_t("Error uploading with TUS resumable uploads")); + } + file.status = Dropzone.ERROR; + }) + .progress(function (e, bytesUploaded, bytesTotal) { + var percentage = (bytesUploaded / bytesTotal) * 100; + self.$progress + .attr("aria-valuenow", percentage) + .css("width", percentage + "%"); + self.$progress.html( + _t("uploading...") + + "
" + + self.formatBytes(bytesUploaded) + + " / " + + self.formatBytes(bytesTotal) + ); + }) + .done(function (url, file) { + file.status = Dropzone.SUCCESS; + self.dropzone.emit("success", file); + self.dropzone.emit("complete", file); + }); + }, + + formatBytes: function (bytes) { + var kb = Math.round(bytes / 1024); + if (kb < 1024) { + return kb + " KiB"; + } + var mb = Math.round(kb / 1024); + if (mb < 1024) { + return mb + " MB"; + } + return Math.round(mb / 1024) + " GB"; + }, + + setPath: function (path) { + var self = this; + self.currentPath = path; + self.options.url = null; + self.options.url = self.dropzone.options.url = self.getUrl(); + }, + + setupRelatedItems: async function ($input) { + const RelatedItems = (await import("../relateditems/relateditems")).default; + + var self = this; + var options = self.options.relatedItems; + options.upload = false; // ensure that related items upload is off. + if (self.options.initialFolder) { + $input.attr("value", self.options.initialFolder); + } + var ri = new RelatedItems($input, options); + ri.$el.on("change", function () { + var result = $(this).select2("data"); + var path = null; + if (result.length > 0) { + path = result[0].path; + } + self.setPath(path); + }); + return ri; + }, +}); diff --git a/src/pat/upload/upload--implementation.test.js b/src/pat/upload/upload--implementation.test.js new file mode 100644 index 000000000..0d53d8752 --- /dev/null +++ b/src/pat/upload/upload--implementation.test.js @@ -0,0 +1,23 @@ +import $ from "jquery"; +import Upload from "./upload--implementation"; + +// pat-structure, pat-relateditems and the contentbrowser app construct upload +// imperatively (`new Upload(el, options)`) and use the instance right away. +// The implementation must therefore be a constructable pattern that exposes its +// methods synchronously — not a config object grafted asynchronously by the thin +// upload.js registration module. +describe("upload implementation", function () { + let $el; + beforeEach(function () { + $el = $('
').appendTo("body"); + }); + afterEach(function () { + $el.remove(); + }); + + it("is constructable and exposes its methods synchronously", function () { + const upload = new Upload($el, { url: "/upload", currentPath: "/a" }); + expect(typeof upload.setPath).toBe("function"); + expect(typeof upload.getUrl).toBe("function"); + }); +}); diff --git a/src/pat/upload/upload.js b/src/pat/upload/upload.js index 920ee5e9e..07c779be7 100644 --- a/src/pat/upload/upload.js +++ b/src/pat/upload/upload.js @@ -1,502 +1,25 @@ import $ from "jquery"; -import _ from "underscore"; -import _t from "../../core/i18n-wrapper"; import Base from "@patternslib/patternslib/src/core/base"; -import logger from "@patternslib/patternslib/src/core/logging"; -import utils from "../../core/utils"; - -const log = logger.getLogger("pat-upload"); - -let Dropzone; +// Thin registration module: the upload implementation (jquery/underscore/i18n +// wiring plus the lazily-loaded Dropzone integration) is loaded on first match, +// so it stays out of the eager patterns chunk on pages without an upload widget. export default Base.extend({ name: "upload", trigger: ".pat-upload", parser: "mockup", - defaults: { - showTitle: true, - url: null, // XXX MUST provide url to submit to OR be in a form - className: "upload", - wrap: false, - wrapperTemplate: '
', - fileaddedClassName: "dropping", - useTus: false, - container: "", - ajaxUpload: true, - - paramName: "file", - addRemoveLinks: false, - autoCleanResults: true, - previewsContainer: ".previews", - previewTemplate: null, - maxFiles: null, - maxFilesize: 99999999, // let's not have a max by default... - - allowPathSelection: undefined, - relatedItems: { - // UID attribute is required here since we're working with related items - attributes: [ - "UID", - "Title", - "Description", - "getURL", - "portal_type", - "path", - "ModificationDate", - ], - batchSize: 20, - basePath: "/", - vocabularyUrl: null, - width: 500, - maximumSelectionSize: 1, - selectableTypes: ["Folder"], - }, - }, init: async function () { - import("dropzone/dist/dropzone.css"); - import("./upload.scss"); - Dropzone = (await import("dropzone")).default; - /* we do not want this plugin to auto discover */ - Dropzone.autoDiscover = false; - - const UploadTemplate = (await import("./templates/upload.xml")).default; - - var self = this; - - if (self.$el.find(".upload-area").length) { - log.info("Found already a dropzone on element, skipping!"); - return; - } - - if (typeof self.options.allowPathSelection === "undefined") { - // Set allowPathSelection to true, if we can use path based urls. - self.options.allowPathSelection = - self.options.baseUrl && self.options.relativePath; - } - - // TODO: find a way to make this work in firefox (and IE) - $(document).on("paste", function (e) { - var oe = e.originalEvent; - var items = oe.clipboardData.items; - if (items) { - for (var i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") !== -1) { - var blob = items[i].getAsFile(); - self.dropzone.addFile(blob); - } - } - } - }); - // values that will change current processing - self.currentPath = self.options.currentPath; - self.currentFile = 0; - - let template = _.template(UploadTemplate)({ - _t: _t, - allowPathSelection: self.options.allowPathSelection, - }); - self.$el.addClass(self.options.className); - self.$el.append(template); - - self.$progress = $(".progress-bar-success", self.$el); - - if (!self.options.showTitle) { - self.$el.find("h2.title").hide(); - } - - if (!self.options.ajaxUpload) { - // no ajax upload, drop the fallback - $(".fallback", this.$el).remove(); - if (this.$el.hasClass(".upload-container")) { - this.$el.addClass("no-ajax-upload"); - } else { - this.$el.closest(".upload-container").addClass("no-ajax-upload"); - } - } - - if (self.options.wrap) { - self.$el.wrap(self.options.wrapperTemplate); - self.$el = self.$el.parent(); - } - - if (self.options.allowPathSelection) { - // only use related items if we can generate path based urls and if it's not turned off. - self.$pathInput = $('input[name="location"]', self.$el); - self.relatedItems = await self.setupRelatedItems(self.$pathInput); - } else { - $('input[name="location"]', self.$el).parent().remove(); - self.relatedItems = null; - } - - self.$dropzone = $(".upload-area", self.$el); - - $("div.browse-select button.browse", self.$el).on("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - if ( - !self.options.maxFiles || - self.dropzone.files.length < self.options.maxFiles - ) { - self.dropzone.hiddenFileInput.click(); - } - }); - - var dzoneOptions = await this.getDzoneOptions(); - - try { - // if init of Dropzone fails it says nothing and - // it fails silently. Using this block we make sure - // that if you break it w/ some weird or missing option - // you can get a proper log of it - // - self.dropzone = new Dropzone(self.$dropzone[0], dzoneOptions); - } catch (e) { - if (window.DEBUG) { - // log it! - console.log(e); - } - throw e; - } - - self.dropzone.on("maxfilesreached", function () { - self.showHideControls(); - }); - - self.dropzone.on("addedfile", function (/* file */) { - self.showHideControls(); - }); - - self.dropzone.on("removedfile", function () { - self.showHideControls(); - }); - - self.dropzone.on("success", function (e, response) { - // Trigger event 'uploadAllCompleted' and pass the server's reponse and - // the path uid. This event can be listened to by patterns using the - // upload pattern, e.g. the TinyMCE pattern's link plugin. - var data; - try { - data = $.parseJSON(response); - } catch { - data = response; - } - self.$el.trigger("uploadAllCompleted", { - data: data, - path_uid: self.$pathInput ? self.$pathInput.val() : null, - }); - }); - - if (self.options.autoCleanResults) { - self.dropzone.on("complete", function (file) { - if (file.status === Dropzone.SUCCESS) { - setTimeout(function () { - $(file.previewElement).fadeOut(); - }, 3000); - } - }); - } - - self.dropzone.on("complete", function (file) { - if (file.status === Dropzone.SUCCESS && self.dropzone.files.length === 1) { - self.showHideControls(); - } - }); - - self.dropzone.on("error", function (file, response, xmlhr) { - if (typeof xmlhr !== "undefined" && xmlhr.status !== 403) { - // If error other than 403, just print a generic message - $(".dz-error-message span", file.previewElement).html( - _t("The file transfer failed") - ); - } - }); - - self.dropzone.on("totaluploadprogress", function (pct) { - // need to caclulate total pct here in reality since we're manually - // processing each file one at a time. - pct = - (((self.currentFile - 1) * 100 + pct) / - (self.dropzone.files.length * 100)) * - 100; - self.$progress.attr("aria-valuenow", pct).css("width", pct + "%"); - }); - - $(".upload-all", self.$el).on("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - self.processUpload({ - finished: function () { - self.$progress.attr("aria-valuenow", 0).css("width", "0%"); - }, - }); - }); - if (self.options.clipboardfile) { - self.dropzone.addFile(self.options.clipboardfile); - } - }, - - showHideControls: function () { - /* we do this delayed because this can be called multiple times - AND we need to do this hide/show AFTER dropzone is done with - all it's own events. This is NASTY but the only way we can - enforce some numFiles with dropzone! */ - var self = this; - if (self._showHideTimeout) { - clearTimeout(self._showHideTimeout); - } - self._showHideTimeout = setTimeout(function () { - self._showHideControls(); - }, 50); - }, - - _showHideControls: function () { - var self = this; - var $controls = $(".controls", self.$el); - var $browse = $(".browse-select", self.$el); - var $input = $(".dz-hidden-input"); - - if (self.options.maxFiles) { - if (self.dropzone.files.length < self.options.maxFiles) { - $browse.show(); - $input.prop("disabled", false); - } else { - $browse.hide(); - $input.prop("disabled", true); - } - } - if (self.dropzone.files.length > 0) { - $controls.fadeIn("slow"); - var file = self.dropzone.files[0]; - $(".dz-error-message span", file.previewElement).html(""); - } else { - $controls.fadeOut("slow"); - } - }, - - pathJoin: function () { - var parts = []; - _.each(arguments, function (part) { - if (!part) { - return; - } - if (part[0] === "/") { - part = part.substring(1); - } - if (part[part.length - 1] === "/") { - part = part.substring(0, part.length - 1); - } - parts.push(part); - }); - return parts.join("/"); - }, - - getUrl: function () { - var self = this; - var url = self.options.url; - if (!url) { - if (self.options.baseUrl && self.options.relativePath) { - url = self.options.baseUrl; - if (url[url.length - 1] !== "/") { - url = url + "/"; - } - url = url + self.pathJoin(self.currentPath, self.options.relativePath); - } else { - var $form = self.$el.parents("form"); - if ($form.length > 0) { - url = $form.attr("action"); - } else { - url = window.location.href; - } - } - } - return url; - }, - - getDzoneOptions: async function () { - let PreviewTemplate = await import("./templates/preview.xml"); - PreviewTemplate = PreviewTemplate.default; - - var self = this; - - // This pattern REQUIRE dropzone to be clickable - self.options.clickable = true; - - var options = $.extend({}, self.options); - options.url = self.getUrl(); - - options.headers = { - "X-CSRF-TOKEN": utils.getAuthenticator(), - }; - - // XXX force to only upload one to the server at a time, - // right now we don't support multiple for backends - options.uploadMultiple = false; - - delete options.wrap; - delete options.wrapperTemplate; - delete options.resultTemplate; - delete options.autoCleanResults; - delete options.fileaddedClassName; - delete options.useTus; - - if (self.options.previewsContainer) { - /* - * if they have a select but it's not an id, let's make an id selector - * so we can target the correct container. dropzone is weird here... - */ - var $preview = self.$el.find(self.options.previewsContainer); - if ($preview.length > 0) { - options.previewsContainer = $preview[0]; - } - } - - // XXX: do we need to allow this? - options.autoProcessQueue = false; - // options.addRemoveLinks = true; // we show them in the template - options.previewTemplate = PreviewTemplate; - - // if our element is a form we should force some values - // https://github.com/enyo/dropzone/wiki/Combine-normal-form-with-Dropzone - return options; - }, - - processUpload: function (options) { - if (!options) { - options = {}; - } - - var self = this, - processing = false, - useTus = self.options.useTus, - fileaddedClassName = self.options.fileaddedClassName, - finished = options.finished; - - self.currentFile = 0; - - function process() { - processing = true; - if (self.dropzone.files.length === 0) { - processing = false; - } - - var file = self.dropzone.files[0]; - if (processing && file.status === Dropzone.ERROR) { - // Put the file back as "queued" for retrying - file.status = Dropzone.QUEUED; - processing = false; - } - - if (!processing) { - self.$el.removeClass(fileaddedClassName); - if (finished !== undefined && typeof finished === "function") { - finished(); - } - return; - } - - if ([Dropzone.SUCCESS, Dropzone.CANCELED].indexOf(file.status) !== -1) { - // remove it - self.dropzone.removeFile(file); - process(); - } else if (file.status !== Dropzone.UPLOADING) { - // start processing file - if (useTus && window.tus) { - // use tus upload if installed - self.handleTusUpload(file); - } else { - // otherwise, just use dropzone to process - self.currentFile += 1; - self.dropzone.processFile(file); - } - setTimeout(process, 100); - } else { - // currently processing - setTimeout(process, 100); - } - } - process(); - }, - - handleTusUpload: function (file) { - /* this needs fixing... */ - var self = this, - chunkSize = 1024 * 1024 * 5; // 5mb chunk size - - file.status = Dropzone.UPLOADING; - - window.tus - .upload(file, { - endpoint: self.dropzone.options.url, - headers: { - "FILENAME": file.name, - "X-CSRF-TOKEN": utils.getAuthenticator(), - }, - chunkSize: chunkSize, - }) - .fail(function () { - if (window.DEBUG) { - console.alert(_t("Error uploading with TUS resumable uploads")); - } - file.status = Dropzone.ERROR; - }) - .progress(function (e, bytesUploaded, bytesTotal) { - var percentage = (bytesUploaded / bytesTotal) * 100; - self.$progress - .attr("aria-valuenow", percentage) - .css("width", percentage + "%"); - self.$progress.html( - _t("uploading...") + - "
" + - self.formatBytes(bytesUploaded) + - " / " + - self.formatBytes(bytesTotal) - ); - }) - .done(function (url, file) { - file.status = Dropzone.SUCCESS; - self.dropzone.emit("success", file); - self.dropzone.emit("complete", file); - }); - }, - - formatBytes: function (bytes) { - var kb = Math.round(bytes / 1024); - if (kb < 1024) { - return kb + " KiB"; - } - var mb = Math.round(kb / 1024); - if (mb < 1024) { - return mb + " MB"; - } - return Math.round(mb / 1024) + " GB"; - }, - - setPath: function (path) { - var self = this; - self.currentPath = path; - self.options.url = null; - self.options.url = self.dropzone.options.url = self.getUrl(); - }, - - setupRelatedItems: async function ($input) { - const RelatedItems = (await import("../relateditems/relateditems")).default; - - var self = this; - var options = self.options.relatedItems; - options.upload = false; // ensure that related items upload is off. - if (self.options.initialFolder) { - $input.attr("value", self.options.initialFolder); - } - var ri = new RelatedItems($input, options); - ri.$el.on("change", function () { - var result = $(this).select2("data"); - var path = null; - if (result.length > 0) { - path = result[0].path; - } - self.setPath(path); - }); - return ri; + // The implementation is a full Base.extend pattern (imperative callers + // do `new Upload(...)`), so its instance behaviour lives on the + // prototype. + const proto = (await import("./upload--implementation")).default.prototype; + // Defaults live with the implementation; merge them under the parsed + // options (which must win), reproducing the eager pattern's options. + this.options = $.extend(true, {}, proto.defaults, this.options); + // Graft onto this single instance so the pattern registers and behaves + // exactly as before, just with the heavy body loaded on demand. + $.extend(this, proto); + return proto.init.apply(this, arguments); }, });