UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

518 lines (458 loc) 20.1 kB
/* Copyright The Infusion copyright holders See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-project/infusion/raw/main/AUTHORS.md. Licensed under the Educational Community License (ECL), Version 2.0 or the New BSD license. You may not use this file except in compliance with one these Licenses. You may obtain a copy of the ECL 2.0 License and BSD License at https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt */ /******************* * File Queue View * *******************/ "use strict"; fluid.registerNamespace("fluid.uploader.fileQueueView"); // Real data binding would be nice to replace these two pairs. fluid.uploader.fileQueueView.rowForFile = function (that, file) { return that.container.find("#" + file.id); }; fluid.uploader.fileQueueView.errorRowForFile = function (that, file) { return $("#" + file.id + "_error", that.container); }; // TODO: None of this hierarchy operates a proper model idiom since it just shares an array instance with fileQueue fluid.uploader.fileQueueView.fileForRow = function (that, row) { return fluid.find_if(that.queueFiles, function (file) { return file.id.toString() === row.prop("id"); }); }; fluid.uploader.fileQueueView.progressorForFile = function (that, file) { var progressId = file.id + "_progress"; return that.fileProgressors[progressId]; }; fluid.uploader.fileQueueView.startFileProgress = function (that, file) { var fileRowElm = fluid.uploader.fileQueueView.rowForFile(that, file); that.scroller.scrollTo(fileRowElm); // update the progressor and make sure that it's in position var fileProgressor = fluid.uploader.fileQueueView.progressorForFile(that, file); fileProgressor.refreshView(); fileProgressor.show(); }; fluid.uploader.fileQueueView.updateFileProgress = function (that, file, fileBytesComplete, fileTotalBytes) { var filePercent = fluid.uploader.derivePercent(fileBytesComplete, fileTotalBytes); var filePercentStr = filePercent + "%"; fluid.uploader.fileQueueView.progressorForFile(that, file).update(filePercent, filePercentStr); }; fluid.uploader.fileQueueView.hideFileProgress = function (that, file) { var fileRowElm = fluid.uploader.fileQueueView.rowForFile(that, file); fluid.uploader.fileQueueView.progressorForFile(that, file).hide(); if (file.filestatus === fluid.uploader.fileStatusConstants.COMPLETE) { that.locate("fileIconBtn", fileRowElm).removeClass(that.options.styles.dim); } }; fluid.uploader.fileQueueView.removeFileProgress = function (that, file) { var fileProgressor = fluid.uploader.fileQueueView.progressorForFile(that, file); if (!fileProgressor) { return; } var rowProgressor = fileProgressor.displayElement; rowProgressor.remove(); }; fluid.uploader.fileQueueView.animateRowRemoval = function (that, row) { row.fadeOut("fast", function () { row.remove(); that.refreshView(); }); }; fluid.uploader.fileQueueView.removeFileErrorRow = function (that, file) { if (file.filestatus === fluid.uploader.fileStatusConstants.ERROR) { fluid.uploader.fileQueueView.animateRowRemoval(that, fluid.uploader.fileQueueView.errorRowForFile(that, file)); } }; fluid.uploader.fileQueueView.removeFileAndRow = function (that, file, row) { // Clean up the stuff associated with a file row. fluid.uploader.fileQueueView.removeFileProgress(that, file); fluid.uploader.fileQueueView.removeFileErrorRow(that, file); // Remove the file itself. that.events.onFileRemoved.fire(file); fluid.uploader.fileQueueView.animateRowRemoval(that, row); }; fluid.uploader.fileQueueView.removeFileForRow = function (that, row) { var file = fluid.uploader.fileQueueView.fileForRow(that, row); if (!file || file.filestatus === fluid.uploader.fileStatusConstants.COMPLETE) { return; } fluid.uploader.fileQueueView.removeFileAndRow(that, file, row); }; fluid.uploader.fileQueueView.removeRowForFile = function (that, file) { var row = fluid.uploader.fileQueueView.rowForFile(that, file); fluid.uploader.fileQueueView.removeFileAndRow(that, file, row); }; fluid.uploader.fileQueueView.bindHover = function (row, styles) { var over = function () { if (row.hasClass(styles.ready) && !row.hasClass(styles.uploading)) { row.addClass(styles.hover); } }; var out = function () { if (row.hasClass(styles.ready) && !row.hasClass(styles.uploading)) { row.removeClass(styles.hover); } }; row.on("mouseenter", over); row.on("mouseleave", out); }; fluid.uploader.fileQueueView.bindDeleteKey = function (that, row) { var deleteHandler = function () { fluid.uploader.fileQueueView.removeFileForRow(that, row); }; fluid.activatable(row, null, { additionalBindings: [{ key: $.ui.keyCode.DELETE, activateHandler: deleteHandler }] }); }; fluid.uploader.fileQueueView.bindRowHandlers = function (that, row) { that.locate("fileIconBtn", row).on("click", function () { fluid.uploader.fileQueueView.removeFileForRow(that, row); }); fluid.uploader.fileQueueView.bindDeleteKey(that, row); }; fluid.uploader.fileQueueView.renderRowFromTemplate = function (that, file) { var row = that.rowTemplate.clone(), fileName = file.name, fileSize = fluid.uploader.formatFileSize(file.size); row.removeClass(that.options.styles.hiddenTemplate); that.locate("fileName", row).text(fileName); that.locate("fileSize", row).text(fileSize); var fileIconBtn = that.locate("fileIconBtn", row); fileIconBtn.addClass(that.options.styles.remove); fluid.updateAriaLabel(fileIconBtn, that.options.strings.buttons.remove); row.prop("id", file.id); row.addClass(that.options.styles.ready); fluid.uploader.fileQueueView.bindRowHandlers(that, row); fluid.updateAriaLabel(row, fileName + " " + fileSize); return row; }; fluid.uploader.fileQueueView.createProgressorFromTemplate = function (that, row) { // create a new progress bar for the row and position it var rowProgressor = that.rowProgressorTemplate.clone(); var rowId = row.prop("id"); var progressId = rowId + "_progress"; rowProgressor.prop("id", progressId); rowProgressor.css("top", row.position().top); rowProgressor.height(row.height()).width(5); that.container.after(rowProgressor); that.fileProgressors[progressId] = fluid.progress(that.options.uploaderContainer, { selectors: { progressBar: "#" + rowId, displayElement: "#" + progressId, label: "#" + progressId + " .fl-uploader-file-progress-text", indicator: "#" + progressId } }); }; fluid.uploader.fileQueueView.addFile = function (that, file) { var row = fluid.uploader.fileQueueView.renderRowFromTemplate(that, file); row.hide(); that.container.append(row); row.attr("title", that.options.strings.status.remove); row.fadeIn("slow"); fluid.uploader.fileQueueView.createProgressorFromTemplate(that, row); that.refreshView(); that.scroller.scrollTo("max"); }; // Toggle keyboard row handlers on and off depending on the uploader state fluid.uploader.fileQueueView.enableRows = function (rows, state) { for (var i = 0; i < rows.length; i++) { fluid.enabled(rows[i], state); } }; fluid.uploader.fileQueueView.prepareForUpload = function (that) { var rowButtons = that.locate("fileIconBtn", that.locate("fileRows")); rowButtons.prop("disabled", true); rowButtons.addClass(that.options.styles.dim); fluid.uploader.fileQueueView.enableRows(that.locate("fileRows"), false); }; fluid.uploader.fileQueueView.refreshAfterUpload = function (that) { var rows = that.locate("fileRows"); var rowButtons = that.locate("fileIconBtn", rows); // only re-enable rowButtons for files that have not been uploaded. rowButtons.each(function (index, rowButton) { // TODO: Improve detection of completed files so as not to rely on row styling. $(rowButton).prop("disabled", rows.eq(index).hasClass(that.options.styles.uploaded)); }); rowButtons.removeClass(that.options.styles.dim); fluid.uploader.fileQueueView.enableRows(that.locate("fileRows"), true); }; fluid.uploader.fileQueueView.changeRowState = function (that, row, newState) { row.removeClass(that.options.styles.ready).removeClass(that.options.styles.error).addClass(newState); }; fluid.uploader.fileQueueView.markRowAsComplete = function (that, file) { // update styles and keyboard bindings for the file row var row = fluid.uploader.fileQueueView.rowForFile(that, file); fluid.uploader.fileQueueView.changeRowState(that, row, that.options.styles.uploaded); row.attr("title", that.options.strings.status.success); fluid.enabled(row, false); // update the click event and the styling for the file delete button var rowButton = that.locate("fileIconBtn", row); rowButton.off("click"); rowButton.removeClass(that.options.styles.remove); rowButton.attr("title", that.options.strings.status.success); }; fluid.uploader.fileQueueView.renderErrorInfoFromTemplate = function (that, fileRow, error) { // Render the row by cloning the template and binding its id to the file. var errorRow = that.errorInfoTemplate.clone(); errorRow.prop("id", fileRow.prop("id") + "_error"); // Look up the error message and render it. var errorType = fluid.keyForValue(fluid.uploader.errorConstants, error); var errorMsg = that.options.strings.errors[errorType]; that.locate("errorText", errorRow).text(errorMsg); that.locate("fileName", fileRow).after(errorRow); that.scroller.scrollTo(errorRow); }; fluid.uploader.fileQueueView.showErrorForFile = function (that, file, error) { fluid.uploader.fileQueueView.hideFileProgress(that, file); if (file.filestatus === fluid.uploader.fileStatusConstants.ERROR) { var fileRowElm = fluid.uploader.fileQueueView.rowForFile(that, file); fluid.uploader.fileQueueView.changeRowState(that, fileRowElm, that.options.styles.error); fluid.uploader.fileQueueView.renderErrorInfoFromTemplate(that, fileRowElm, error); } }; fluid.uploader.fileQueueView.addKeyboardNavigation = function (that) { fluid.tabbable(that.container); that.selectableContext = fluid.selectable(that.container, { selectableSelector: that.options.selectors.fileRows, onSelect: function (itemToSelect) { $(itemToSelect).addClass(that.options.styles.selected); }, onUnselect: function (selectedItem) { $(selectedItem).removeClass(that.options.styles.selected); } }); }; fluid.uploader.fileQueueView.prepareTemplateElements = function (that) { // Grab our template elements out of the DOM. that.errorInfoTemplate = that.locate("errorInfoTemplate").remove(); that.errorInfoTemplate.removeClass(that.options.styles.hiddenTemplate); that.rowTemplate = that.locate("rowTemplate").remove(); that.rowProgressorTemplate = that.locate("rowProgressorTemplate", that.options.uploaderContainer).remove(); }; fluid.uploader.fileQueueView.markFileComplete = function (that, file) { fluid.uploader.fileQueueView.progressorForFile(that, file).update(100, "100%"); fluid.uploader.fileQueueView.markRowAsComplete(that, file); }; fluid.uploader.fileQueueView.refreshView = function (that) { that.selectableContext.refresh(); that.scroller.refreshView(); }; /** * Creates a new File Queue view. * * @param {jQuery|selector} container - the file queue's container DOM element * @param {fileQueue} queue - a file queue model instance * @param {Object} options - (optional) configuration options for the view */ fluid.defaults("fluid.uploader.fileQueueView", { gradeNames: ["fluid.viewComponent"], mergePolicy: { // TODO: This mergePolicy was required by some attempts at fixing FLUID-5668 // and may be required again in future if this component is not modelised // "members.queueFiles": "nomerge" }, members: { dom: "@expand:fluid.createLocalContainerDomBinder({that}.container, {that}.options.selectors)", // Uses historical contract via "localContainer" argument fileProgressors: {} // queueFiles: applied in uploader options - TODO: no model idiom }, invokers: { addFile: { funcName: "fluid.uploader.fileQueueView.addFile", args: ["{that}", "{arguments}.0"] // file }, removeFile: { funcName: "fluid.uploader.fileQueueView.removeRowForFile", args: ["{that}", "{arguments}.0"] // file }, prepareForUpload: { funcName: "fluid.uploader.fileQueueView.prepareForUpload", args: "{that}" }, refreshAfterUpload: { funcName: "fluid.uploader.fileQueueView.refreshAfterUpload", args: "{that}" }, showFileProgress: { funcName: "fluid.uploader.fileQueueView.startFileProgress", args: ["{that}", "{arguments}.0"] // file }, updateFileProgress: { funcName: "fluid.uploader.fileQueueView.updateFileProgress", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // file, fileBytesComplete, fileTotalBytes }, markFileComplete: { funcName: "fluid.uploader.fileQueueView.markFileComplete", args: ["{that}", "{arguments}.0"] // file }, showErrorForFile: { funcName: "fluid.uploader.fileQueueView.showErrorForFile", args: ["{that}", "{arguments}.0", "{arguments}.1"] // file, error }, hideFileProgress: { funcName: "fluid.uploader.fileQueueView.hideFileProgress", args: ["{that}", "{arguments}.0"] // file }, refreshView: { funcName: "fluid.uploader.fileQueueView.refreshView", args: "{that}" } }, components: { scroller: { type: "fluid.scrollableTable", container: "{fileQueueView}.container" } }, selectors: { fileRows: ".flc-uploader-file", fileName: ".flc-uploader-file-name", fileSize: ".flc-uploader-file-size", fileIconBtn: ".flc-uploader-file-action", errorText: ".flc-uploader-file-error", rowTemplate: ".flc-uploader-file-tmplt", errorInfoTemplate: ".flc-uploader-file-error-tmplt", rowProgressorTemplate: ".flc-uploader-file-progressor-tmplt" }, styles: { hover: "fl-uploader-file-hover", selected: "fl-uploader-file-focus", ready: "fl-uploader-file-state-ready", uploading: "fl-uploader-file-state-uploading", uploaded: "fl-uploader-file-state-uploaded", error: "fl-uploader-file-state-error", remove: "fl-uploader-file-action-remove", dim: "fl-uploader-dim", hiddenTemplate: "fl-uploader-hidden-templates" }, strings: { progress: { toUploadLabel: "To upload: %fileCount %fileLabel (%totalBytes)", singleFile: "file", pluralFiles: "files" }, status: { success: "File Uploaded", error: "File Upload Error", remove: "Press Delete key to remove file" }, errors: { HTTP_ERROR: "File upload error: a network error occured or the file was rejected (reason unknown).", IO_ERROR: "File upload error: a network error occured.", UPLOAD_LIMIT_EXCEEDED: "File upload error: you have uploaded as many files as you are allowed during this session", UPLOAD_FAILED: "File upload error: the upload failed for an unknown reason.", QUEUE_LIMIT_EXCEEDED: "You have as many files in the queue as can be added at one time. Removing files from the queue may allow you to add different files.", FILE_EXCEEDS_SIZE_LIMIT: "One or more of the files that you attempted to add to the queue exceeded the limit of %fileSizeLimit.", ZERO_BYTE_FILE: "One or more of the files that you attempted to add contained no data.", INVALID_FILETYPE: "One or more files were not added to the queue because they were of the wrong type." }, buttons: { remove: "Remove" } }, events: { onFileRemoved: null }, listeners: { "onCreate.prepareTemplateElement": "fluid.uploader.fileQueueView.prepareTemplateElements", "onCreate.addKeyboardNavigation": "fluid.uploader.fileQueueView.addKeyboardNavigation", "onCreate.addAriaRole": { "this": "{that}.container", method: "attr", args: { role: "application" } } } }); /** * An interactional mixin for binding a fileQueueView to an Uploader */ fluid.defaults("fluid.uploader.fileQueueView.bindUploader", { events: { onFileRemoved: "{uploader}.events.onFileRemoved" }, listeners: { "{uploader}.events.afterFileQueued": "{fileQueueView}.addFile", "{uploader}.events.onUploadStart": "{fileQueueView}.prepareForUpload", "{uploader}.events.onFileStart": "{fileQueueView}.showFileProgress", "{uploader}.events.onFileProgress": "{fileQueueView}.updateFileProgress", "{uploader}.events.onFileSuccess": "{fileQueueView}.markFileComplete", "{uploader}.events.onFileError": "{fileQueueView}.showErrorForFile", "{uploader}.events.afterFileComplete": "{fileQueueView}.hideFileProgress", "{uploader}.events.afterUploadComplete": "{fileQueueView}.refreshAfterUpload" } }); /************** * Scrollable * **************/ fluid.registerNamespace("fluid.scrollable"); fluid.scrollable.makeSimple = function (element) { return fluid.container(element); }; fluid.scrollable.makeTable = function (table, wrapperMarkup) { table.wrap(wrapperMarkup); return table.closest(".fl-scrollable-scroller"); }; /* * Simple component cover for the jQuery scrollTo plugin. Provides roughly equivalent * functionality to Uploader's old Scroller plugin. */ fluid.defaults("fluid.scrollable", { gradeNames: ["fluid.viewComponent"], makeScrollableFn: fluid.scrollable.makeSimple, // NB - a modern style would configure an invoker members: { scrollable: { expander: { func: "{that}.options.makeScrollableFn", args: ["{that}.container", "{that}.options.wrapperMarkup"] // TODO: we need to make sure that expander arguments are evaluated fully } }, maxHeight: { expander: { "this": "{that}.scrollable", method: "css", args: "max-height" } } }, invokers: { /** * Programmatically scrolls this scrollable element to the region specified. * This method is directly compatible with the underlying jQuery.scrollTo plugin. */ scrollTo: { "this": "{that}.scrollable", method: "scrollTo", args: "{arguments}.0" }, /** * Updates the view of the scrollable region. This should be called when the content of the scrollable region is changed. */ refreshView: { funcName: "fluid.identity" } }, listeners: { onCreate: "{that}.refreshView" } }); /* * Wraps a table in order to make it scrollable with the jQuery.scrollTo plugin. * Container divs are injected to allow cross-browser support. */ fluid.defaults("fluid.scrollableTable", { gradeNames: ["fluid.scrollable"], makeScrollableFn: fluid.scrollable.makeTable, wrapperMarkup: "<div class='fl-scrollable-scroller'><div class='fl-scrollable-inner'></div></div>" });