infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
518 lines (458 loc) • 20.1 kB
JavaScript
/*
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>"
});