You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12438 lines
391 KiB

/*
* Copyright (c) 2014
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/* global dragOptions, folderDropOptions, OC */
(function() {
if (!OCA.Files) {
/**
* Namespace for the files app
* @namespace OCA.Files
*/
OCA.Files = {};
}
/**
* @namespace OCA.Files.App
*/
OCA.Files.App = {
/**
* Navigation control
*
* @member {OCA.Files.Navigation}
*/
navigation: null,
/**
* File list for the "All files" section.
*
* @member {OCA.Files.FileList}
*/
fileList: null,
/**
* Backbone model for storing files preferences
*/
_filesConfig: null,
/**
* Initializes the files app
*/
initialize: function() {
this.navigation = new OCA.Files.Navigation($('#app-navigation'));
this.$showHiddenFiles = $('input#showhiddenfilesToggle');
var showHidden = $('#showHiddenFiles').val() === "1";
this.$showHiddenFiles.prop('checked', showHidden);
if ($('#fileNotFound').val() === "1") {
OC.Notification.show(t('files', 'File could not be found'), {type: 'error'});
}
this._filesConfig = new OC.Backbone.Model({
showhidden: showHidden
});
var urlParams = OC.Util.History.parseUrlQuery();
var fileActions = new OCA.Files.FileActions();
// default actions
fileActions.registerDefaultActions();
// legacy actions
fileActions.merge(window.FileActions);
// regular actions
fileActions.merge(OCA.Files.fileActions);
this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
OCA.Files.fileActions.on('setDefault.app-files', this._onActionsUpdated);
OCA.Files.fileActions.on('registerAction.app-files', this._onActionsUpdated);
window.FileActions.on('setDefault.app-files', this._onActionsUpdated);
window.FileActions.on('registerAction.app-files', this._onActionsUpdated);
this.files = OCA.Files.Files;
// TODO: ideally these should be in a separate class / app (the embedded "all files" app)
this.fileList = new OCA.Files.FileList(
$('#app-content-files'), {
dragOptions: dragOptions,
folderDropOptions: folderDropOptions,
fileActions: fileActions,
allowLegacyActions: true,
scrollTo: urlParams.scrollto,
filesClient: OC.Files.getClient(),
multiSelectMenu: [
{
name: 'copyMove',
displayName: t('files', 'Move or copy'),
iconClass: 'icon-external',
},
{
name: 'download',
displayName: t('files', 'Download'),
iconClass: 'icon-download',
},
OCA.Files.FileList.MultiSelectMenuActions.ToggleSelectionModeAction,
{
name: 'delete',
displayName: t('files', 'Delete'),
iconClass: 'icon-delete',
},
],
sorting: {
mode: $('#defaultFileSorting').val(),
direction: $('#defaultFileSortingDirection').val()
},
config: this._filesConfig,
enableUpload: true,
maxChunkSize: OC.appConfig.files && OC.appConfig.files.max_chunk_size
}
);
this.files.initialize();
// for backward compatibility, the global FileList will
// refer to the one of the "files" view
window.FileList = this.fileList;
OC.Plugins.attach('OCA.Files.App', this);
this._setupEvents();
// trigger URL change event handlers
this._onPopState(urlParams);
$('#quota.has-tooltip').tooltip({
placement: 'top'
});
this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200);
if (sessionStorage.getItem('WhatsNewServerCheck') < (Date.now() - 3600*1000)) {
OCP.WhatsNew.query(); // for Nextcloud server
sessionStorage.setItem('WhatsNewServerCheck', Date.now());
}
},
/**
* Destroy the app
*/
destroy: function() {
this.navigation = null;
this.fileList.destroy();
this.fileList = null;
this.files = null;
OCA.Files.fileActions.off('setDefault.app-files', this._onActionsUpdated);
OCA.Files.fileActions.off('registerAction.app-files', this._onActionsUpdated);
window.FileActions.off('setDefault.app-files', this._onActionsUpdated);
window.FileActions.off('registerAction.app-files', this._onActionsUpdated);
},
_onActionsUpdated: function(ev) {
// forward new action to the file list
if (ev.action) {
this.fileList.fileActions.registerAction(ev.action);
} else if (ev.defaultAction) {
this.fileList.fileActions.setDefault(
ev.defaultAction.mime,
ev.defaultAction.name
);
}
},
/**
* Returns the container of the currently visible app.
*
* @return app container
*/
getCurrentAppContainer: function() {
return this.navigation.getActiveContainer();
},
/**
* Sets the currently active view
* @param viewId view id
*/
setActiveView: function(viewId, options) {
this.navigation.setActiveItem(viewId, options);
},
/**
* Returns the view id of the currently active view
* @return view id
*/
getActiveView: function() {
return this.navigation.getActiveItem();
},
/**
*
* @returns {Backbone.Model}
*/
getFilesConfig: function() {
return this._filesConfig;
},
/**
* Setup events based on URL changes
*/
_setupEvents: function() {
OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this));
// detect when app changed their current directory
$('#app-content').delegate('>div', 'changeDirectory', _.bind(this._onDirectoryChanged, this));
$('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this));
$('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this));
$('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this));
this.$showHiddenFiles.on('change', _.bind(this._onShowHiddenFilesChange, this));
},
/**
* Toggle showing hidden files according to the settings checkbox
*
* @returns {undefined}
*/
_onShowHiddenFilesChange: function() {
var show = this.$showHiddenFiles.is(':checked');
this._filesConfig.set('showhidden', show);
this._debouncedPersistShowHiddenFilesState();
},
/**
* Persist show hidden preference on the server
*
* @returns {undefined}
*/
_persistShowHiddenFilesState: function() {
var show = this._filesConfig.get('showhidden');
$.post(OC.generateUrl('/apps/files/api/v1/showhidden'), {
show: show
});
},
/**
* Event handler for when the current navigation item has changed
*/
_onNavigationChanged: function(e) {
var params;
if (e && e.itemId) {
params = {
view: typeof e.view === 'string' && e.view !== '' ? e.view : e.itemId,
dir: e.dir ? e.dir : '/'
};
this._changeUrl(params.view, params.dir);
OC.Apps.hideAppSidebar($('.detailsView'));
this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params));
}
},
/**
* Event handler for when an app notified that its directory changed
*/
_onDirectoryChanged: function(e) {
if (e.dir) {
this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId);
}
},
/**
* Event handler for when an app notified that its directory changed
*/
_onAfterDirectoryChanged: function(e) {
if (e.dir && e.fileId) {
this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId);
}
},
/**
* Event handler for when an app notifies that it needs space
* for viewer mode.
*/
_onChangeViewerMode: function(e) {
var state = !!e.viewerModeEnabled;
if (e.viewerModeEnabled) {
OC.Apps.hideAppSidebar($('.detailsView'));
}
$('#app-navigation').toggleClass('hidden', state);
$('.app-files').toggleClass('viewer-mode no-sidebar', state);
},
/**
* Event handler for when the URL changed
*/
_onPopState: function(params) {
params = _.extend({
dir: '/',
view: 'files'
}, params);
var lastId = this.navigation.getActiveItem();
if (!this.navigation.itemExists(params.view)) {
params.view = 'files';
}
this.navigation.setActiveItem(params.view, {silent: true});
if (lastId !== this.navigation.getActiveItem()) {
this.navigation.getActiveContainer().trigger(new $.Event('show'));
}
this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params));
},
/**
* Encode URL params into a string, except for the "dir" attribute
* that gets encoded as path where "/" is not encoded
*
* @param {Object.<string>} params
* @return {string} encoded params
*/
_makeUrlParams: function(params) {
var dir = params.dir;
delete params.dir;
return 'dir=' + OC.encodePath(dir) + '&' + OC.buildQueryString(params);
},
/**
* Change the URL to point to the given dir and view
*/
_changeUrl: function(view, dir, fileId) {
var params = {dir: dir};
if (view !== 'files') {
params.view = view;
} else if (fileId) {
params.fileid = fileId;
}
var currentParams = OC.Util.History.parseUrlQuery();
if (currentParams.dir === params.dir && currentParams.view === params.view && currentParams.fileid !== params.fileid) {
// if only fileid changed or was added, replace instead of push
OC.Util.History.replaceState(this._makeUrlParams(params));
} else {
OC.Util.History.pushState(this._makeUrlParams(params));
}
}
};
})();
$(document).ready(function() {
// wait for other apps/extensions to register their event handlers and file actions
// in the "ready" clause
_.defer(function() {
OCA.Files.App.initialize();
});
});
(function() {
var template = Handlebars.template, templates = OCA.Files.Templates = OCA.Files.Templates || {};
templates['detailsview'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1;
return "<ul class=\"tabHeaders\">\n"
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tabHeaders : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":4,"column":1},"end":{"line":9,"column":10}}})) != null ? stack1 : "")
+ "</ul>\n";
},"2":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <li class=\"tabHeader\" data-tabid=\""
+ alias4(((helper = (helper = helpers.tabId || (depth0 != null ? depth0.tabId : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"tabId","hash":{},"data":data,"loc":{"start":{"line":5,"column":35},"end":{"line":5,"column":44}}}) : helper)))
+ "\" tabindex=\"0\">\n "
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tabIcon : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":6,"column":5},"end":{"line":6,"column":65}}})) != null ? stack1 : "")
+ "\n <a href=\"#\" tabindex=\"-1\">"
+ alias4(((helper = (helper = helpers.label || (depth0 != null ? depth0.label : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"label","hash":{},"data":data,"loc":{"start":{"line":7,"column":28},"end":{"line":7,"column":37}}}) : helper)))
+ "</a>\n </li>\n";
},"3":function(container,depth0,helpers,partials,data) {
var helper;
return "<span class=\"icon "
+ container.escapeExpression(((helper = (helper = helpers.tabIcon || (depth0 != null ? depth0.tabIcon : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"tabIcon","hash":{},"data":data,"loc":{"start":{"line":6,"column":38},"end":{"line":6,"column":49}}}) : helper)))
+ "\"></span>";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});
return "<div class=\"detailFileInfoContainer\"></div>\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tabHeaders : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":2,"column":0},"end":{"line":11,"column":7}}})) != null ? stack1 : "")
+ "<div class=\"tabsContainer\"></div>\n<a class=\"close icon-close\" href=\"#\"><span class=\"hidden-visually\">"
+ container.escapeExpression(((helper = (helper = helpers.closeLabel || (depth0 != null ? depth0.closeLabel : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"closeLabel","hash":{},"data":data,"loc":{"start":{"line":13,"column":67},"end":{"line":13,"column":81}}}) : helper)))
+ "</span></a>\n";
},"useData":true});
templates['favorite_mark'] = template({"1":function(container,depth0,helpers,partials,data) {
return "permanent";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, buffer =
"<div class=\"favorite-mark ";
stack1 = ((helper = (helper = helpers.isFavorite || (depth0 != null ? depth0.isFavorite : depth0)) != null ? helper : alias2),(options={"name":"isFavorite","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":26},"end":{"line":1,"column":65}}}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));
if (!helpers.isFavorite) { stack1 = container.hooks.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "\">\n <span class=\"icon "
+ alias4(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconClass","hash":{},"data":data,"loc":{"start":{"line":2,"column":19},"end":{"line":2,"column":32}}}) : helper)))
+ "\" />\n <span class=\"hidden-visually\">"
+ alias4(((helper = (helper = helpers.altText || (depth0 != null ? depth0.altText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altText","hash":{},"data":data,"loc":{"start":{"line":3,"column":31},"end":{"line":3,"column":42}}}) : helper)))
+ "</span>\n</div>\n";
},"useData":true});
templates['file_action_trigger'] = template({"1":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <img class=\"svg\" alt=\""
+ alias4(((helper = (helper = helpers.altText || (depth0 != null ? depth0.altText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altText","hash":{},"data":data,"loc":{"start":{"line":3,"column":24},"end":{"line":3,"column":35}}}) : helper)))
+ "\" src=\""
+ alias4(((helper = (helper = helpers.icon || (depth0 != null ? depth0.icon : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"icon","hash":{},"data":data,"loc":{"start":{"line":3,"column":42},"end":{"line":3,"column":50}}}) : helper)))
+ "\" />\n";
},"3":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.iconClass : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":5,"column":2},"end":{"line":7,"column":9}}})) != null ? stack1 : "")
+ ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasDisplayName : depth0),{"name":"unless","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":8,"column":2},"end":{"line":10,"column":13}}})) != null ? stack1 : "");
},"4":function(container,depth0,helpers,partials,data) {
var helper;
return " <span class=\"icon "
+ container.escapeExpression(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"iconClass","hash":{},"data":data,"loc":{"start":{"line":6,"column":21},"end":{"line":6,"column":34}}}) : helper)))
+ "\" />\n";
},"6":function(container,depth0,helpers,partials,data) {
var helper;
return " <span class=\"hidden-visually\">"
+ container.escapeExpression(((helper = (helper = helpers.altText || (depth0 != null ? depth0.altText : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"altText","hash":{},"data":data,"loc":{"start":{"line":9,"column":33},"end":{"line":9,"column":44}}}) : helper)))
+ "</span>\n";
},"8":function(container,depth0,helpers,partials,data) {
var helper;
return "<span> "
+ container.escapeExpression(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"displayName","hash":{},"data":data,"loc":{"start":{"line":12,"column":27},"end":{"line":12,"column":42}}}) : helper)))
+ "</span>";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<a class=\"action action-"
+ alias4(((helper = (helper = helpers.nameLowerCase || (depth0 != null ? depth0.nameLowerCase : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"nameLowerCase","hash":{},"data":data,"loc":{"start":{"line":1,"column":24},"end":{"line":1,"column":41}}}) : helper)))
+ "\" href=\"#\" data-action=\""
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":1,"column":65},"end":{"line":1,"column":73}}}) : helper)))
+ "\">\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.icon : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.program(3, data, 0),"data":data,"loc":{"start":{"line":2,"column":1},"end":{"line":11,"column":8}}})) != null ? stack1 : "")
+ " "
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.displayName : depth0),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":12,"column":1},"end":{"line":12,"column":56}}})) != null ? stack1 : "")
+ "\n</a>\n";
},"useData":true});
templates['fileactionsmenu'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <li class=\""
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.inline : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":13},"end":{"line":3,"column":40}}})) != null ? stack1 : "")
+ " action-"
+ alias4(((helper = (helper = helpers.nameLowerCase || (depth0 != null ? depth0.nameLowerCase : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"nameLowerCase","hash":{},"data":data,"loc":{"start":{"line":3,"column":48},"end":{"line":3,"column":65}}}) : helper)))
+ "-container\">\n <a href=\"#\" class=\"menuitem action action-"
+ alias4(((helper = (helper = helpers.nameLowerCase || (depth0 != null ? depth0.nameLowerCase : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"nameLowerCase","hash":{},"data":data,"loc":{"start":{"line":4,"column":45},"end":{"line":4,"column":62}}}) : helper)))
+ " permanent\" data-action=\""
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":4,"column":87},"end":{"line":4,"column":95}}}) : helper)))
+ "\">\n "
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.icon : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.program(6, data, 0),"data":data,"loc":{"start":{"line":5,"column":4},"end":{"line":12,"column":11}}})) != null ? stack1 : "")
+ " <span>"
+ alias4(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"displayName","hash":{},"data":data,"loc":{"start":{"line":13,"column":10},"end":{"line":13,"column":25}}}) : helper)))
+ "</span>\n </a>\n </li>\n";
},"2":function(container,depth0,helpers,partials,data) {
return "hidden";
},"4":function(container,depth0,helpers,partials,data) {
var helper;
return "<img class=\"icon\" src=\""
+ container.escapeExpression(((helper = (helper = helpers.icon || (depth0 != null ? depth0.icon : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"icon","hash":{},"data":data,"loc":{"start":{"line":5,"column":39},"end":{"line":5,"column":47}}}) : helper)))
+ "\"/>\n";
},"6":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.iconClass : depth0),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.program(9, data, 0),"data":data,"loc":{"start":{"line":7,"column":5},"end":{"line":11,"column":12}}})) != null ? stack1 : "");
},"7":function(container,depth0,helpers,partials,data) {
var helper;
return " <span class=\"icon "
+ container.escapeExpression(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"iconClass","hash":{},"data":data,"loc":{"start":{"line":8,"column":24},"end":{"line":8,"column":37}}}) : helper)))
+ "\"></span>\n";
},"9":function(container,depth0,helpers,partials,data) {
return " <span class=\"no-icon\"></span>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1;
return "<ul>\n"
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":2,"column":1},"end":{"line":16,"column":10}}})) != null ? stack1 : "")
+ "</ul>\n";
},"useData":true});
templates['filemultiselectmenu'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <li class=\"item-"
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":3,"column":18},"end":{"line":3,"column":26}}}) : helper)))
+ "\">\n <a href=\"#\" class=\"menuitem action "
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":4,"column":38},"end":{"line":4,"column":46}}}) : helper)))
+ " permanent\" data-action=\""
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":4,"column":71},"end":{"line":4,"column":79}}}) : helper)))
+ "\">\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.iconClass : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data,"loc":{"start":{"line":5,"column":4},"end":{"line":9,"column":11}}})) != null ? stack1 : "")
+ " <span class=\"label\">"
+ alias4(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"displayName","hash":{},"data":data,"loc":{"start":{"line":10,"column":24},"end":{"line":10,"column":39}}}) : helper)))
+ "</span>\n </a>\n </li>\n";
},"2":function(container,depth0,helpers,partials,data) {
var helper;
return " <span class=\"icon "
+ container.escapeExpression(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"iconClass","hash":{},"data":data,"loc":{"start":{"line":6,"column":23},"end":{"line":6,"column":36}}}) : helper)))
+ "\"></span>\n";
},"4":function(container,depth0,helpers,partials,data) {
return " <span class=\"no-icon\"></span>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1;
return "<ul>\n"
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":2,"column":1},"end":{"line":13,"column":10}}})) != null ? stack1 : "")
+ "</ul>\n";
},"useData":true});
templates['filesummary'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper;
return "<span class=\"info\">\n <span class=\"dirinfo\"></span>\n <span class=\"connector\">"
+ container.escapeExpression(((helper = (helper = helpers.connectorLabel || (depth0 != null ? depth0.connectorLabel : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"connectorLabel","hash":{},"data":data,"loc":{"start":{"line":3,"column":25},"end":{"line":3,"column":43}}}) : helper)))
+ "</span>\n <span class=\"fileinfo\"></span>\n <span class=\"hiddeninfo\"></span>\n <span class=\"filter\"></span>\n</span>\n";
},"useData":true});
templates['mainfileinfodetailsview'] = template({"1":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <a href=\"#\" class=\"action action-favorite favorite permanent\">\n <span class=\"icon "
+ alias4(((helper = (helper = helpers.starClass || (depth0 != null ? depth0.starClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"starClass","hash":{},"data":data,"loc":{"start":{"line":13,"column":22},"end":{"line":13,"column":35}}}) : helper)))
+ "\" title=\""
+ alias4(((helper = (helper = helpers.starAltText || (depth0 != null ? depth0.starAltText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"starAltText","hash":{},"data":data,"loc":{"start":{"line":13,"column":44},"end":{"line":13,"column":59}}}) : helper)))
+ "\"></span>\n </a>\n";
},"3":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<span class=\"size\" title=\""
+ alias4(((helper = (helper = helpers.altSize || (depth0 != null ? depth0.altSize : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altSize","hash":{},"data":data,"loc":{"start":{"line":16,"column":43},"end":{"line":16,"column":54}}}) : helper)))
+ "\">"
+ alias4(((helper = (helper = helpers.size || (depth0 != null ? depth0.size : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"size","hash":{},"data":data,"loc":{"start":{"line":16,"column":56},"end":{"line":16,"column":64}}}) : helper)))
+ "</span>, ";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<div class=\"thumbnailContainer\"><a href=\"#\" class=\"thumbnail action-default\"><div class=\"stretcher\"/></a></div>\n<div class=\"file-details-container\">\n <div class=\"fileName\">\n <h3 title=\""
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":4,"column":13},"end":{"line":4,"column":21}}}) : helper)))
+ "\" class=\"ellipsis\">"
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":4,"column":40},"end":{"line":4,"column":48}}}) : helper)))
+ "</h3>\n <a class=\"permalink\" href=\""
+ alias4(((helper = (helper = helpers.permalink || (depth0 != null ? depth0.permalink : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalink","hash":{},"data":data,"loc":{"start":{"line":5,"column":29},"end":{"line":5,"column":42}}}) : helper)))
+ "\" title=\""
+ alias4(((helper = (helper = helpers.permalinkTitle || (depth0 != null ? depth0.permalinkTitle : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalinkTitle","hash":{},"data":data,"loc":{"start":{"line":5,"column":51},"end":{"line":5,"column":69}}}) : helper)))
+ "\" data-clipboard-text=\""
+ alias4(((helper = (helper = helpers.permalink || (depth0 != null ? depth0.permalink : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalink","hash":{},"data":data,"loc":{"start":{"line":5,"column":92},"end":{"line":5,"column":105}}}) : helper)))
+ "\">\n <span class=\"icon icon-clippy\"></span>\n <span class=\"hidden-visually\">"
+ alias4(((helper = (helper = helpers.permalinkTitle || (depth0 != null ? depth0.permalinkTitle : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalinkTitle","hash":{},"data":data,"loc":{"start":{"line":7,"column":33},"end":{"line":7,"column":51}}}) : helper)))
+ "</span>\n </a>\n </div>\n <div class=\"file-details ellipsis\">\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.hasFavoriteAction : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":11,"column":2},"end":{"line":15,"column":9}}})) != null ? stack1 : "")
+ " "
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.hasSize : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":16,"column":2},"end":{"line":16,"column":80}}})) != null ? stack1 : "")
+ "<span class=\"date live-relative-timestamp\" data-timestamp=\""
+ alias4(((helper = (helper = helpers.timestamp || (depth0 != null ? depth0.timestamp : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"timestamp","hash":{},"data":data,"loc":{"start":{"line":16,"column":139},"end":{"line":16,"column":152}}}) : helper)))
+ "\" title=\""
+ alias4(((helper = (helper = helpers.altDate || (depth0 != null ? depth0.altDate : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altDate","hash":{},"data":data,"loc":{"start":{"line":16,"column":161},"end":{"line":16,"column":172}}}) : helper)))
+ "\">"
+ alias4(((helper = (helper = helpers.date || (depth0 != null ? depth0.date : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"date","hash":{},"data":data,"loc":{"start":{"line":16,"column":174},"end":{"line":16,"column":182}}}) : helper)))
+ "</span>\n </div>\n</div>\n<div class=\"hidden permalink-field\">\n <input type=\"text\" value=\""
+ alias4(((helper = (helper = helpers.permalink || (depth0 != null ? depth0.permalink : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalink","hash":{},"data":data,"loc":{"start":{"line":20,"column":27},"end":{"line":20,"column":40}}}) : helper)))
+ "\" placeholder=\""
+ alias4(((helper = (helper = helpers.permalinkTitle || (depth0 != null ? depth0.permalinkTitle : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalinkTitle","hash":{},"data":data,"loc":{"start":{"line":20,"column":55},"end":{"line":20,"column":73}}}) : helper)))
+ "\" readonly=\"readonly\"/>\n</div>\n";
},"useData":true});
templates['newfilemenu'] = template({"1":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <li>\n <a href=\"#\" class=\"menuitem\" data-templatename=\""
+ alias4(((helper = (helper = helpers.templateName || (depth0 != null ? depth0.templateName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"templateName","hash":{},"data":data,"loc":{"start":{"line":7,"column":51},"end":{"line":7,"column":67}}}) : helper)))
+ "\" data-filetype=\""
+ alias4(((helper = (helper = helpers.fileType || (depth0 != null ? depth0.fileType : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileType","hash":{},"data":data,"loc":{"start":{"line":7,"column":84},"end":{"line":7,"column":96}}}) : helper)))
+ "\" data-action=\""
+ alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data,"loc":{"start":{"line":7,"column":111},"end":{"line":7,"column":117}}}) : helper)))
+ "\"><span class=\"icon "
+ alias4(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconClass","hash":{},"data":data,"loc":{"start":{"line":7,"column":137},"end":{"line":7,"column":150}}}) : helper)))
+ " svg\"></span><span class=\"displayname\">"
+ alias4(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"displayName","hash":{},"data":data,"loc":{"start":{"line":7,"column":189},"end":{"line":7,"column":204}}}) : helper)))
+ "</span></a>\n </li>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<ul>\n <li>\n <label for=\"file_upload_start\" class=\"menuitem\" data-action=\"upload\" title=\""
+ alias4(((helper = (helper = helpers.uploadMaxHumanFilesize || (depth0 != null ? depth0.uploadMaxHumanFilesize : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"uploadMaxHumanFilesize","hash":{},"data":data,"loc":{"start":{"line":3,"column":78},"end":{"line":3,"column":104}}}) : helper)))
+ "\" tabindex=\"0\"><span class=\"svg icon icon-upload\"></span><span class=\"displayname\">"
+ alias4(((helper = (helper = helpers.uploadLabel || (depth0 != null ? depth0.uploadLabel : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"uploadLabel","hash":{},"data":data,"loc":{"start":{"line":3,"column":187},"end":{"line":3,"column":202}}}) : helper)))
+ "</span></label>\n </li>\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":5,"column":1},"end":{"line":9,"column":10}}})) != null ? stack1 : "")
+ "</ul>\n";
},"useData":true});
templates['newfilemenu_filename_form'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<form class=\"filenameform\">\n <input id=\""
+ alias4(((helper = (helper = helpers.cid || (depth0 != null ? depth0.cid : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"cid","hash":{},"data":data,"loc":{"start":{"line":2,"column":12},"end":{"line":2,"column":19}}}) : helper)))
+ "-input-"
+ alias4(((helper = (helper = helpers.fileType || (depth0 != null ? depth0.fileType : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileType","hash":{},"data":data,"loc":{"start":{"line":2,"column":26},"end":{"line":2,"column":38}}}) : helper)))
+ "\" type=\"text\" value=\""
+ alias4(((helper = (helper = helpers.fileName || (depth0 != null ? depth0.fileName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileName","hash":{},"data":data,"loc":{"start":{"line":2,"column":59},"end":{"line":2,"column":71}}}) : helper)))
+ "\" autocomplete=\"off\" autocapitalize=\"off\">\n <input type=\"submit\" value=\" \" class=\"icon-confirm\" />\n</form>\n";
},"useData":true});
templates['operationprogressbar'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper;
return "<div id=\"uploadprogressbar\">\n <em class=\"label outer\" style=\"display:none\"></em>\n</div>\n<button class=\"stop icon-close\" style=\"display:none\">\n <span class=\"hidden-visually\">"
+ container.escapeExpression(((helper = (helper = helpers.textCancelButton || (depth0 != null ? depth0.textCancelButton : depth0)) != null ? helper : container.hooks.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"textCancelButton","hash":{},"data":data,"loc":{"start":{"line":5,"column":31},"end":{"line":5,"column":51}}}) : helper)))
+ "</span>\n</button>\n";
},"useData":true});
templates['operationprogressbarlabel'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<em class=\"label\">\n <span class=\"desktop\">"
+ alias4(((helper = (helper = helpers.textDesktop || (depth0 != null ? depth0.textDesktop : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"textDesktop","hash":{},"data":data,"loc":{"start":{"line":2,"column":23},"end":{"line":2,"column":38}}}) : helper)))
+ "</span>\n <span class=\"mobile\">"
+ alias4(((helper = (helper = helpers.textMobile || (depth0 != null ? depth0.textMobile : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"textMobile","hash":{},"data":data,"loc":{"start":{"line":3,"column":22},"end":{"line":3,"column":36}}}) : helper)))
+ "</span>\n</em>\n";
},"useData":true});
templates['template_addbutton'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<a href=\"#\" class=\"button new\">\n <span class=\"icon "
+ alias4(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconClass","hash":{},"data":data,"loc":{"start":{"line":2,"column":19},"end":{"line":2,"column":32}}}) : helper)))
+ "\"></span>\n <span class=\"hidden-visually\">"
+ alias4(((helper = (helper = helpers.addText || (depth0 != null ? depth0.addText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"addText","hash":{},"data":data,"loc":{"start":{"line":3,"column":31},"end":{"line":3,"column":42}}}) : helper)))
+ "</span>\n</a>\n";
},"useData":true});
})();
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/**
* The file upload code uses several hooks to interact with blueimps jQuery file upload library:
* 1. the core upload handling hooks are added when initializing the plugin,
* 2. if the browser supports progress events they are added in a separate set after the initialization
* 3. every app can add it's own triggers for fileupload
* - files adds d'n'd handlers and also reacts to done events to add new rows to the filelist
* - TODO pictures upload button
* - TODO music upload button
*/
/* global jQuery, humanFileSize, md5 */
/**
* File upload object
*
* @class OC.FileUpload
* @classdesc
*
* Represents a file upload
*
* @param {OC.Uploader} uploader uploader
* @param {Object} data blueimp data
*/
OC.FileUpload = function(uploader, data) {
this.uploader = uploader;
this.data = data;
var basePath = '';
if (this.uploader.fileList) {
basePath = this.uploader.fileList.getCurrentDirectory();
}
var path = OC.joinPaths(basePath, this.getFile().relativePath || '', this.getFile().name);
this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime();
};
OC.FileUpload.CONFLICT_MODE_DETECT = 0;
OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1;
OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2;
OC.FileUpload.prototype = {
/**
* Unique upload id
*
* @type string
*/
id: null,
/**
* Upload element
*
* @type Object
*/
$uploadEl: null,
/**
* Target folder
*
* @type string
*/
_targetFolder: '',
/**
* @type int
*/
_conflictMode: OC.FileUpload.CONFLICT_MODE_DETECT,
/**
* New name from server after autorename
*
* @type String
*/
_newName: null,
/**
* Returns the unique upload id
*
* @return string
*/
getId: function() {
return this.id;
},
/**
* Returns the file to be uploaded
*
* @return {File} file
*/
getFile: function() {
return this.data.files[0];
},
/**
* Return the final filename.
*
* @return {String} file name
*/
getFileName: function() {
// autorenamed name
if (this._newName) {
return this._newName;
}
return this.getFile().name;
},
setTargetFolder: function(targetFolder) {
this._targetFolder = targetFolder;
},
getTargetFolder: function() {
return this._targetFolder;
},
/**
* Get full path for the target file, including relative path,
* without the file name.
*
* @return {String} full path
*/
getFullPath: function() {
return OC.joinPaths(this._targetFolder, this.getFile().relativePath || '');
},
/**
* Get full path for the target file,
* including relative path and file name.
*
* @return {String} full path
*/
getFullFilePath: function() {
return OC.joinPaths(this.getFullPath(), this.getFile().name);
},
/**
* Returns conflict resolution mode.
*
* @return {int} conflict mode
*/
getConflictMode: function() {
return this._conflictMode || OC.FileUpload.CONFLICT_MODE_DETECT;
},
/**
* Set conflict resolution mode.
* See CONFLICT_MODE_* constants.
*
* @param {int} mode conflict mode
*/
setConflictMode: function(mode) {
this._conflictMode = mode;
},
deleteUpload: function() {
delete this.data.jqXHR;
},
/**
* Trigger autorename and append "(2)".
* Multiple calls will increment the appended number.
*/
autoRename: function() {
var name = this.getFile().name;
if (!this._renameAttempt) {
this._renameAttempt = 1;
}
var dotPos = name.lastIndexOf('.');
var extPart = '';
if (dotPos > 0) {
this._newName = name.substr(0, dotPos);
extPart = name.substr(dotPos);
} else {
this._newName = name;
}
// generate new name
this._renameAttempt++;
this._newName = this._newName + ' (' + this._renameAttempt + ')' + extPart;
},
/**
* Submit the upload
*/
submit: function() {
var self = this;
var data = this.data;
var file = this.getFile();
if (self.aborted === true) {
return $.Deferred().resolve().promise();
}
// it was a folder upload, so make sure the parent directory exists already
var folderPromise;
if (file.relativePath) {
folderPromise = this.uploader.ensureFolderExists(this.getFullPath());
} else {
folderPromise = $.Deferred().resolve().promise();
}
if (this.uploader.fileList) {
this.data.url = this.uploader.fileList.getUploadUrl(this.getFileName(), this.getFullPath());
}
if (!this.data.headers) {
this.data.headers = {};
}
// webdav without multipart
this.data.multipart = false;
this.data.type = 'PUT';
delete this.data.headers['If-None-Match'];
if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT
|| this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
this.data.headers['If-None-Match'] = '*';
}
var userName = this.uploader.davClient.getUserName();
var password = this.uploader.davClient.getPassword();
if (userName) {
// copy username/password from DAV client
this.data.headers['Authorization'] =
'Basic ' + btoa(userName + ':' + (password || ''));
}
var chunkFolderPromise;
if ($.support.blobSlice
&& this.uploader.fileUploadParam.maxChunkSize
&& this.getFile().size > this.uploader.fileUploadParam.maxChunkSize
) {
data.isChunked = true;
chunkFolderPromise = this.uploader.davClient.createDirectory(
'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
);
// TODO: if fails, it means same id already existed, need to retry
} else {
chunkFolderPromise = $.Deferred().resolve().promise();
var mtime = this.getFile().lastModified;
if (mtime) {
data.headers['X-OC-Mtime'] = mtime / 1000;
}
}
// wait for creation of the required directory before uploading
return Promise.all([folderPromise, chunkFolderPromise]).then(function() {
if (self.aborted !== true) {
data.submit();
}
}, function() {
self.abort();
});
},
/**
* Process end of transfer
*/
done: function() {
if (!this.data.isChunked) {
return $.Deferred().resolve().promise();
}
var uid = OC.getCurrentUser().uid;
var mtime = this.getFile().lastModified;
var size = this.getFile().size;
var headers = {};
if (mtime) {
headers['X-OC-Mtime'] = mtime / 1000;
}
if (size) {
headers['OC-Total-Length'] = size;
}
return this.uploader.davClient.move(
'uploads/' + uid + '/' + this.getId() + '/.file',
'files/' + uid + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()),
true,
headers
);
},
_deleteChunkFolder: function() {
// delete transfer directory for this upload
this.uploader.davClient.remove(
'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
);
},
/**
* Abort the upload
*/
abort: function() {
if (this.data.isChunked) {
this._deleteChunkFolder();
}
this.data.abort();
this.deleteUpload();
this.aborted = true;
},
/**
* Fail the upload
*/
fail: function() {
this.deleteUpload();
if (this.data.isChunked) {
this._deleteChunkFolder();
}
},
/**
* Returns the server response
*
* @return {Object} response
*/
getResponse: function() {
var response = this.data.response();
if (response.errorThrown) {
// attempt parsing Sabre exception is available
var xml = response.jqXHR.responseXML;
if (xml && xml.documentElement.localName === 'error' && xml.documentElement.namespaceURI === 'DAV:') {
var messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message');
var exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception');
if (messages.length) {
response.message = messages[0].textContent;
}
if (exceptions.length) {
response.exception = exceptions[0].textContent;
}
return response;
}
}
if (typeof response.result !== 'string' && response.result) {
//fetch response from iframe
response = $.parseJSON(response.result[0].body.innerText);
if (!response) {
// likely due to internal server error
response = {status: 500};
}
} else {
response = response.result;
}
return response;
},
/**
* Returns the status code from the response
*
* @return {int} status code
*/
getResponseStatus: function() {
if (this.uploader.isXHRUpload()) {
var xhr = this.data.response().jqXHR;
if (xhr) {
return xhr.status;
}
return null;
}
return this.getResponse().status;
},
/**
* Returns the response header by name
*
* @param {String} headerName header name
* @return {Array|String} response header value(s)
*/
getResponseHeader: function(headerName) {
headerName = headerName.toLowerCase();
if (this.uploader.isXHRUpload()) {
return this.data.response().jqXHR.getResponseHeader(headerName);
}
var headers = this.getResponse().headers;
if (!headers) {
return null;
}
var value = _.find(headers, function(value, key) {
return key.toLowerCase() === headerName;
});
if (_.isArray(value) && value.length === 1) {
return value[0];
}
return value;
}
};
/**
* keeps track of uploads in progress and implements callbacks for the conflicts dialog
* @namespace
*/
OC.Uploader = function() {
this.init.apply(this, arguments);
};
OC.Uploader.prototype = _.extend({
/**
* @type Array<OC.FileUpload>
*/
_uploads: {},
/**
* Count of upload done promises that have not finished yet.
*
* @type int
*/
_pendingUploadDoneCount: 0,
/**
* Is it currently uploading?
*
* @type boolean
*/
_uploading: false,
/**
* List of directories known to exist.
*
* Key is the fullpath and value is boolean, true meaning that the directory
* was already created so no need to create it again.
*/
_knownDirs: {},
/**
* @type OCA.Files.FileList
*/
fileList: null,
/**
* @type OCA.Files.OperationProgressBar
*/
progressBar: null,
/**
* @type OC.Files.Client
*/
filesClient: null,
/**
* Webdav client pointing at the root "dav" endpoint
*
* @type OC.Files.Client
*/
davClient: null,
/**
* Function that will allow us to know if Ajax uploads are supported
* @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html
* also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata
*/
_supportAjaxUploadWithProgress: function() {
if (window.TESTING) {
return true;
}
return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData();
// Is the File API supported?
function supportFileAPI() {
var fi = document.createElement('INPUT');
fi.type = 'file';
return 'files' in fi;
}
// Are progress events supported?
function supportAjaxUploadProgressEvents() {
var xhr = new XMLHttpRequest();
return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload));
}
// Is FormData supported?
function supportFormData() {
return !! window.FormData;
}
},
/**
* Returns whether an XHR upload will be used
*
* @return {bool} true if XHR upload will be used,
* false for iframe upload
*/
isXHRUpload: function () {
return !this.fileUploadParam.forceIframeTransport &&
((!this.fileUploadParam.multipart && $.support.xhrFileUpload) ||
$.support.xhrFormDataFileUpload);
},
/**
* Makes sure that the upload folder and its parents exists
*
* @param {String} fullPath full path
* @return {Promise} promise that resolves when all parent folders
* were created
*/
ensureFolderExists: function(fullPath) {
if (!fullPath || fullPath === '/') {
return $.Deferred().resolve().promise();
}
// remove trailing slash
if (fullPath.charAt(fullPath.length - 1) === '/') {
fullPath = fullPath.substr(0, fullPath.length - 1);
}
var self = this;
var promise = this._knownDirs[fullPath];
if (this.fileList) {
// assume the current folder exists
this._knownDirs[this.fileList.getCurrentDirectory()] = $.Deferred().resolve().promise();
}
if (!promise) {
var deferred = new $.Deferred();
promise = deferred.promise();
this._knownDirs[fullPath] = promise;
// make sure all parents already exist
var parentPath = OC.dirname(fullPath);
var parentPromise = this._knownDirs[parentPath];
if (!parentPromise) {
parentPromise = this.ensureFolderExists(parentPath);
}
parentPromise.then(function() {
self.filesClient.createDirectory(fullPath).always(function(status) {
// 405 is expected if the folder already exists
if ((status >= 200 && status < 300) || status === 405) {
if (status !== 405) {
self.trigger('createdfolder', fullPath);
}
deferred.resolve();
return;
}
OC.Notification.show(t('files', 'Could not create folder "{dir}"', {dir: fullPath}), {type: 'error'});
deferred.reject();
});
}, function() {
deferred.reject();
});
}
return promise;
},
/**
* Submit the given uploads
*
* @param {Array} array of uploads to start
*/
submitUploads: function(uploads) {
var self = this;
_.each(uploads, function(upload) {
self._uploads[upload.data.uploadId] = upload;
});
self.totalToUpload = _.reduce(uploads, function(memo, upload) { return memo+upload.getFile().size; }, 0);
var semaphore = new OCA.Files.Semaphore(5);
var promises = _.map(uploads, function(upload) {
return semaphore.acquire().then(function(){
return upload.submit().then(function(){
semaphore.release();
});
});
});
},
confirmBeforeUnload: function() {
if (this._uploading) {
return t('files', 'This will stop your current uploads.')
}
},
/**
* Show conflict for the given file object
*
* @param {OC.FileUpload} file upload object
*/
showConflict: function(fileUpload) {
//show "file already exists" dialog
var self = this;
var file = fileUpload.getFile();
// already attempted autorename but the server said the file exists ? (concurrently added)
if (fileUpload.getConflictMode() === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
// attempt another autorename, defer to let the current callback finish
_.defer(function() {
self.onAutorename(fileUpload);
});
return;
}
// retrieve more info about this file
this.filesClient.getFileInfo(fileUpload.getFullFilePath()).then(function(status, fileInfo) {
var original = fileInfo;
var replacement = file;
original.directory = original.path;
OC.dialogs.fileexists(fileUpload, original, replacement, self);
});
},
/**
* cancels all uploads
*/
cancelUploads:function() {
this.log('canceling uploads');
jQuery.each(this._uploads, function(i, upload) {
upload.abort();
});
this.clear();
},
/**
* Clear uploads
*/
clear: function() {
this._knownDirs = {};
},
/**
* Returns an upload by id
*
* @param {int} data uploadId
* @return {OC.FileUpload} file upload
*/
getUpload: function(data) {
if (_.isString(data)) {
return this._uploads[data];
} else if (data.uploadId && this._uploads[data.uploadId]) {
this._uploads[data.uploadId].data = data;
return this._uploads[data.uploadId];
}
return null;
},
/**
* Removes an upload from the list of known uploads.
*
* @param {OC.FileUpload} upload the upload to remove.
*/
removeUpload: function(upload) {
if (!upload || !upload.data || !upload.data.uploadId) {
return;
}
delete this._uploads[upload.data.uploadId];
},
showUploadCancelMessage: _.debounce(function() {
OC.Notification.show(t('files', 'Upload cancelled.'), {timeout : 7, type: 'error'});
}, 500),
/**
* callback for the conflicts dialog
*/
onCancel:function() {
this.cancelUploads();
},
/**
* callback for the conflicts dialog
* calls onSkip, onReplace or onAutorename for each conflict
* @param {object} conflicts - list of conflict elements
*/
onContinue:function(conflicts) {
var self = this;
//iterate over all conflicts
jQuery.each(conflicts, function (i, conflict) {
conflict = $(conflict);
var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1;
var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1;
if (keepOriginal && keepReplacement) {
// when both selected -> autorename
self.onAutorename(conflict.data('data'));
} else if (keepReplacement) {
// when only replacement selected -> overwrite
self.onReplace(conflict.data('data'));
} else {
// when only original selected -> skip
// when none selected -> skip
self.onSkip(conflict.data('data'));
}
});
},
/**
* handle skipping an upload
* @param {OC.FileUpload} upload
*/
onSkip:function(upload) {
this.log('skip', null, upload);
upload.deleteUpload();
},
/**
* handle replacing a file on the server with an uploaded file
* @param {FileUpload} data
*/
onReplace:function(upload) {
this.log('replace', null, upload);
upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE);
this.submitUploads([upload]);
},
/**
* handle uploading a file and letting the server decide a new name
* @param {object} upload
*/
onAutorename:function(upload) {
this.log('autorename', null, upload);
upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME);
do {
upload.autoRename();
// if file known to exist on the client side, retry
} while (this.fileList && this.fileList.inList(upload.getFileName()));
// resubmit upload
this.submitUploads([upload]);
},
_trace: false, //TODO implement log handler for JS per class?
log: function(caption, e, data) {
if (this._trace) {
console.log(caption);
console.log(data);
}
},
/**
* checks the list of existing files prior to uploading and shows a simple dialog to choose
* skip all, replace all or choose which files to keep
*
* @param {array} selection of files to upload
* @param {object} callbacks - object with several callback methods
* @param {function} callbacks.onNoConflicts
* @param {function} callbacks.onSkipConflicts
* @param {function} callbacks.onReplaceConflicts
* @param {function} callbacks.onChooseConflicts
* @param {function} callbacks.onCancel
*/
checkExistingFiles: function (selection, callbacks) {
var fileList = this.fileList;
var conflicts = [];
// only keep non-conflicting uploads
selection.uploads = _.filter(selection.uploads, function(upload) {
var file = upload.getFile();
if (file.relativePath) {
// can't check in subfolder contents
return true;
}
if (!fileList) {
// no list to check against
return true;
}
var fileInfo = fileList.findFile(file.name);
if (fileInfo) {
conflicts.push([
// original
_.extend(fileInfo, {
directory: fileInfo.directory || fileInfo.path || fileList.getCurrentDirectory()
}),
// replacement (File object)
upload
]);
return false;
}
return true;
});
if (conflicts.length) {
// wait for template loading
OC.dialogs.fileexists(null, null, null, this).done(function() {
_.each(conflicts, function(conflictData) {
OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this);
});
});
}
// upload non-conflicting files
// note: when reaching the server they might still meet conflicts
// if the folder was concurrently modified, these will get added
// to the already visible dialog, if applicable
callbacks.onNoConflicts(selection);
},
_updateProgressBarOnUploadStop: function() {
if (this._pendingUploadDoneCount === 0) {
// All the uploads ended and there is no pending operation, so hide
// the progress bar.
// Note that this happens here only with non-chunked uploads; if the
// upload was chunked then this will have been executed after all
// the uploads ended but before the upload done handler that reduces
// the pending operation count was executed.
this._hideProgressBar();
return;
}
this._setProgressBarText(t('files', 'Processing files …'), t('files', '…'));
// Nothing is being uploaded at this point, and the pending operations
// can not be cancelled, so the cancel button should be hidden.
this._hideCancelButton();
},
_hideProgressBar: function() {
this.progressBar.hideProgressBar();
},
_hideCancelButton: function() {
this.progressBar.hideCancelButton();
},
_showProgressBar: function() {
this.progressBar.showProgressBar();
},
_setProgressBarValue: function(value) {
this.progressBar.setProgressBarValue(value);
},
_setProgressBarText: function(textDesktop, textMobile, title) {
this.progressBar.setProgressBarText(textDesktop, textMobile, title);
},
/**
* Returns whether the given file is known to be a received shared file
*
* @param {Object} file file
* @return {bool} true if the file is a shared file
*/
_isReceivedSharedFile: function(file) {
if (!window.FileList) {
return false;
}
var $tr = window.FileList.findFileEl(file.name);
if (!$tr.length) {
return false;
}
return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory');
},
/**
* Initialize the upload object
*
* @param {Object} $uploadEl upload element
* @param {Object} options
* @param {OCA.Files.FileList} [options.fileList] file list object
* @param {OC.Files.Client} [options.filesClient] files client object
* @param {Object} [options.dropZone] drop zone for drag and drop upload
*/
init: function($uploadEl, options) {
var self = this;
options = options || {};
this.fileList = options.fileList;
this.progressBar = options.progressBar;
this.filesClient = options.filesClient || OC.Files.getClient();
this.davClient = new OC.Files.Client({
host: this.filesClient.getHost(),
root: OC.linkToRemoteBase('dav'),
useHTTPS: OC.getProtocol() === 'https',
userName: this.filesClient.getUserName(),
password: this.filesClient.getPassword()
});
$uploadEl = $($uploadEl);
this.$uploadEl = $uploadEl;
if ($uploadEl.exists()) {
this.progressBar.on('cancel', function() {
self.cancelUploads();
});
this.fileUploadParam = {
type: 'PUT',
dropZone: options.dropZone, // restrict dropZone to content div
autoUpload: false,
sequentialUploads: false,
limitConcurrentUploads: 10,
/**
* on first add of every selection
* - check all files of originalFiles array with files in dir
* - on conflict show dialog
* - skip all -> remember as single skip action for all conflicting files
* - replace all -> remember as single replace action for all conflicting files
* - choose -> show choose dialog
* - mark files to keep
* - when only existing -> remember as single skip action
* - when only new -> remember as single replace action
* - when both -> remember as single autorename action
* - start uploading selection
* @param {object} e
* @param {object} data
* @returns {boolean}
*/
add: function(e, data) {
self.log('add', e, data);
var that = $(this), freeSpace;
var upload = new OC.FileUpload(self, data);
// can't link directly due to jQuery not liking cyclic deps on its ajax object
data.uploadId = upload.getId();
// create a container where we can store the data objects
if ( ! data.originalFiles.selection ) {
// initialize selection and remember number of files to upload
data.originalFiles.selection = {
uploads: [],
filesToUpload: data.originalFiles.length,
totalBytes: 0
};
}
// TODO: move originalFiles to a separate container, maybe inside OC.Upload
var selection = data.originalFiles.selection;
// add uploads
if ( selection.uploads.length < selection.filesToUpload ) {
// remember upload
selection.uploads.push(upload);
}
//examine file
var file = upload.getFile();
try {
// FIXME: not so elegant... need to refactor that method to return a value
Files.isFileNameValid(file.name);
}
catch (errorMessage) {
data.textStatus = 'invalidcharacters';
data.errorThrown = errorMessage;
}
if (data.targetDir) {
upload.setTargetFolder(data.targetDir);
delete data.targetDir;
}
// in case folder drag and drop is not supported file will point to a directory
// http://stackoverflow.com/a/20448357
if ( ! file.type && file.size % 4096 === 0 && file.size <= 102400) {
var dirUploadFailure = false;
try {
var reader = new FileReader();
reader.readAsBinaryString(file);
} catch (NS_ERROR_FILE_ACCESS_DENIED) {
//file is a directory
dirUploadFailure = true;
}
if (dirUploadFailure) {
data.textStatus = 'dirorzero';
data.errorThrown = t('files',
'Unable to upload {filename} as it is a directory or has 0 bytes',
{filename: file.name}
);
}
}
// only count if we're not overwriting an existing shared file
if (self._isReceivedSharedFile(file)) {
file.isReceivedShare = true;
} else {
// add size
selection.totalBytes += file.size;
}
// check free space
freeSpace = $('#free_space').val();
if (freeSpace >= 0 && selection.totalBytes > freeSpace) {
data.textStatus = 'notenoughspace';
data.errorThrown = t('files',
'Not enough free space, you are uploading {size1} but only {size2} is left', {
'size1': humanFileSize(selection.totalBytes),
'size2': humanFileSize($('#free_space').val())
});
}
// end upload for whole selection on error
if (data.errorThrown) {
// trigger fileupload fail handler
var fu = that.data('blueimp-fileupload') || that.data('fileupload');
fu._trigger('fail', e, data);
return false; //don't upload anything
}
// check existing files when all is collected
if ( selection.uploads.length >= selection.filesToUpload ) {
//remove our selection hack:
delete data.originalFiles.selection;
var callbacks = {
onNoConflicts: function (selection) {
self.submitUploads(selection.uploads);
},
onSkipConflicts: function (selection) {
//TODO mark conflicting files as toskip
},
onReplaceConflicts: function (selection) {
//TODO mark conflicting files as toreplace
},
onChooseConflicts: function (selection) {
//TODO mark conflicting files as chosen
},
onCancel: function (selection) {
$.each(selection.uploads, function(i, upload) {
upload.abort();
});
}
};
self.checkExistingFiles(selection, callbacks);
}
return true; // continue adding files
},
/**
* called after the first add, does NOT have the data param
* @param {object} e
*/
start: function(e) {
self.log('start', e, null);
//hide the tooltip otherwise it covers the progress bar
$('#upload').tooltip('hide');
self._uploading = true;
},
fail: function(e, data) {
var upload = self.getUpload(data);
var status = null;
if (upload) {
status = upload.getResponseStatus();
}
self.log('fail', e, upload);
self.removeUpload(upload);
if (data.textStatus === 'abort' || data.errorThrown === 'abort') {
self.showUploadCancelMessage();
} else if (status === 412) {
// file already exists
self.showConflict(upload);
} else if (status === 404) {
// target folder does not exist any more
OC.Notification.show(t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()} ), {type: 'error'});
self.cancelUploads();
} else if (data.textStatus === 'notenoughspace') {
// not enough space
OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'});
self.cancelUploads();
} else {
// HTTP connection problem or other error
var message = t('files', 'An unknown error has occurred');
if (upload) {
var response = upload.getResponse();
if (response) {
message = response.message;
}
}
OC.Notification.show(message || data.errorThrown, {type: 'error'});
}
if (upload) {
upload.fail();
}
},
/**
* called for every successful upload
* @param {object} e
* @param {object} data
*/
done:function(e, data) {
var upload = self.getUpload(data);
var that = $(this);
self.log('done', e, upload);
self.removeUpload(upload);
var status = upload.getResponseStatus();
if (status < 200 || status >= 300) {
// trigger fail handler
var fu = that.data('blueimp-fileupload') || that.data('fileupload');
fu._trigger('fail', e, data);
return;
}
},
/**
* called after last upload
* @param {object} e
* @param {object} data
*/
stop: function(e, data) {
self.log('stop', e, data);
self._uploading = false;
}
};
if (options.maxChunkSize) {
this.fileUploadParam.maxChunkSize = options.maxChunkSize;
}
// initialize jquery fileupload (blueimp)
var fileupload = this.$uploadEl.fileupload(this.fileUploadParam);
if (this._supportAjaxUploadWithProgress()) {
//remaining time
var lastUpdate, lastSize, bufferSize, buffer, bufferIndex, bufferIndex2, bufferTotal;
var dragging = false;
// add progress handlers
fileupload.on('fileuploadadd', function(e, data) {
self.log('progress handle fileuploadadd', e, data);
self.trigger('add', e, data);
});
// add progress handlers
fileupload.on('fileuploadstart', function(e, data) {
self.log('progress handle fileuploadstart', e, data);
self._setProgressBarText(t('files', 'Uploading …'), t('files', '…'));
self._setProgressBarValue(0);
self._showProgressBar();
// initial remaining time variables
lastUpdate = new Date().getTime();
lastSize = 0;
bufferSize = 20;
buffer = [];
bufferIndex = 0;
bufferIndex2 = 0;
bufferTotal = 0;
for(var i = 0; i < bufferSize; i++){
buffer[i] = 0;
}
self.trigger('start', e, data);
});
fileupload.on('fileuploadprogress', function(e, data) {
self.log('progress handle fileuploadprogress', e, data);
//TODO progressbar in row
self.trigger('progress', e, data);
});
fileupload.on('fileuploadprogressall', function(e, data) {
self.log('progress handle fileuploadprogressall', e, data);
var total = self.totalToUpload;
var progress = (data.loaded / total) * 100;
var thisUpdate = new Date().getTime();
var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s
lastUpdate = thisUpdate;
var diffSize = data.loaded - lastSize;
lastSize = data.loaded;
diffSize = diffSize / diffUpdate; // apply timing factor, eg. 1MiB/2s = 0.5MiB/s, unit is byte per second
var remainingSeconds = ((total - data.loaded) / diffSize);
if(remainingSeconds >= 0) {
bufferTotal = bufferTotal - (buffer[bufferIndex]) + remainingSeconds;
buffer[bufferIndex] = remainingSeconds; //buffer to make it smoother
bufferIndex = (bufferIndex + 1) % bufferSize;
bufferIndex2++;
}
var smoothRemainingSeconds;
if (bufferIndex2 > 0 && bufferIndex2 < 20) {
smoothRemainingSeconds = bufferTotal / bufferIndex2;
} else if (bufferSize > 0) {
smoothRemainingSeconds = bufferTotal / bufferSize;
} else {
smoothRemainingSeconds = 1;
}
var h = moment.duration(smoothRemainingSeconds, "seconds").humanize();
if (!(smoothRemainingSeconds >= 0 && smoothRemainingSeconds < 14400)) {
// show "Uploading ..." for durations longer than 4 hours
h = t('files', 'Uploading …');
}
self._setProgressBarText(h, h, t('files', '{loadedSize} of {totalSize} ({bitrate})' , {
loadedSize: humanFileSize(data.loaded),
totalSize: humanFileSize(total),
bitrate: humanFileSize(data.bitrate / 8) + '/s'
}));
self._setProgressBarValue(progress);
self.trigger('progressall', e, data);
});
fileupload.on('fileuploadstop', function(e, data) {
self.log('progress handle fileuploadstop', e, data);
self.clear();
self._updateProgressBarOnUploadStop();
self.trigger('stop', e, data);
});
fileupload.on('fileuploadfail', function(e, data) {
self.log('progress handle fileuploadfail', e, data);
self.trigger('fail', e, data);
});
fileupload.on('fileuploaddragover', function(e){
$('#app-content').addClass('file-drag');
$('#emptycontent .icon-folder').addClass('icon-filetype-folder-drag-accept');
var filerow = $(e.delegatedEvent.target).closest('tr');
if(!filerow.hasClass('dropping-to-dir')){
$('.dropping-to-dir .icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
$('.dropping-to-dir').removeClass('dropping-to-dir');
$('.dir-drop').removeClass('dir-drop');
}
if(filerow.attr('data-type') === 'dir'){
$('#app-content').addClass('dir-drop');
filerow.addClass('dropping-to-dir');
filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept');
}
dragging = true;
});
var disableDropState = function() {
$('#app-content').removeClass('file-drag');
$('.dropping-to-dir').removeClass('dropping-to-dir');
$('.dir-drop').removeClass('dir-drop');
$('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
dragging = false;
};
fileupload.on('fileuploaddragleave fileuploaddrop', disableDropState);
// In some browsers the "drop" event can be triggered with no
// files even if the "dragover" event seemed to suggest that a
// file was being dragged (and thus caused "fileuploaddragover"
// to be triggered).
fileupload.on('fileuploaddropnofiles', function() {
if (!dragging) {
return;
}
disableDropState();
OC.Notification.show(t('files', 'Uploading that item is not supported'), {type: 'error'});
});
fileupload.on('fileuploadchunksend', function(e, data) {
// modify the request to adjust it to our own chunking
var upload = self.getUpload(data);
var range = data.contentRange.split(' ')[1];
var chunkId = range.split('/')[0].split('-')[0];
data.url = OC.getRootPath() +
'/remote.php/dav/uploads' +
'/' + OC.getCurrentUser().uid +
'/' + upload.getId() +
'/' + chunkId;
delete data.contentRange;
delete data.headers['Content-Range'];
});
fileupload.on('fileuploaddone', function(e, data) {
var upload = self.getUpload(data);
self._pendingUploadDoneCount++;
upload.done().then(function() {
self._pendingUploadDoneCount--;
if (Object.keys(self._uploads).length === 0 && self._pendingUploadDoneCount === 0) {
// All the uploads ended and there is no pending
// operation, so hide the progress bar.
// Note that this happens here only with chunked
// uploads; if the upload was non-chunked then this
// handler is immediately executed, before the
// jQuery upload done handler that removes the
// upload from the list, and thus at this point
// there is still at least one upload that has not
// ended (although the upload stop handler is always
// executed after all the uploads have ended, which
// hides the progress bar in that case).
self._hideProgressBar();
}
self.trigger('done', e, upload);
}).fail(function(status, response) {
var message = response.message;
if (status === 507) {
// not enough space
OC.Notification.show(message || t('files', 'Not enough free space'), {type: 'error'});
self.cancelUploads();
} else if (status === 409) {
OC.Notification.show(message || t('files', 'Target folder does not exist any more'), {type: 'error'});
} else {
OC.Notification.show(message || t('files', 'Error when assembling chunks, status code {status}', {status: status}), {type: 'error'});
}
self.trigger('fail', e, data);
});
});
fileupload.on('fileuploaddrop', function(e, data) {
self.trigger('drop', e, data);
if (e.isPropagationStopped()) {
return false;
}
});
}
window.onbeforeunload = function() {
return self.confirmBeforeUnload();
}
}
//add multiply file upload attribute to all browsers except konqueror (which crashes when it's used)
if (navigator.userAgent.search(/konqueror/i) === -1) {
this.$uploadEl.attr('multiple', 'multiple');
}
return this.fileUploadParam;
}
}, OC.Backbone.Events);
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/* global Files */
(function() {
/**
* Construct a new NewFileMenu instance
* @constructs NewFileMenu
*
* @memberof OCA.Files
*/
var NewFileMenu = OC.Backbone.View.extend({
tagName: 'div',
// Menu is opened by default because it's rendered on "add-button" click
className: 'newFileMenu popovermenu bubble menu open menu-left',
events: {
'click .menuitem': '_onClickAction'
},
/**
* @type OCA.Files.FileList
*/
fileList: null,
initialize: function(options) {
var self = this;
var $uploadEl = $('#file_upload_start');
if ($uploadEl.length) {
$uploadEl.on('fileuploadstart', function() {
self.trigger('actionPerformed', 'upload');
});
} else {
console.warn('Missing upload element "file_upload_start"');
}
this.fileList = options && options.fileList;
this._menuItems = [{
id: 'folder',
displayName: t('files', 'New folder'),
templateName: t('files', 'New folder'),
iconClass: 'icon-folder',
fileType: 'folder',
actionHandler: function(name) {
self.fileList.createDirectory(name);
}
}];
OC.Plugins.attach('OCA.Files.NewFileMenu', this);
},
template: function(data) {
return OCA.Files.Templates['newfilemenu'](data);
},
/**
* Event handler whenever an action has been clicked within the menu
*
* @param {Object} event event object
*/
_onClickAction: function(event) {
var $target = $(event.target);
if (!$target.hasClass('menuitem')) {
$target = $target.closest('.menuitem');
}
var action = $target.attr('data-action');
// note: clicking the upload label will automatically
// set the focus on the "file_upload_start" hidden field
// which itself triggers the upload dialog.
// Currently the upload logic is still in file-upload.js and filelist.js
if (action === 'upload') {
OC.hideMenus();
} else {
event.preventDefault();
this.$el.find('.menuitem.active').removeClass('active');
$target.addClass('active');
this._promptFileName($target);
}
},
_promptFileName: function($target) {
var self = this;
if ($target.find('form').length) {
$target.find('input[type=\'text\']').focus();
return;
}
// discard other forms
this.$el.find('form').remove();
this.$el.find('.displayname').removeClass('hidden');
$target.find('.displayname').addClass('hidden');
var newName = $target.attr('data-templatename');
var fileType = $target.attr('data-filetype');
var $form = $(OCA.Files.Templates['newfilemenu_filename_form']({
fileName: newName,
cid: this.cid,
fileType: fileType
}));
//this.trigger('actionPerformed', action);
$target.append($form);
// here comes the OLD code
var $input = $form.find('input[type=\'text\']');
var $submit = $form.find('input[type=\'submit\']');
var lastPos;
var checkInput = function () {
var filename = $input.val();
try {
if (!Files.isFileNameValid(filename)) {
// Files.isFileNameValid(filename) throws an exception itself
} else if (self.fileList.inList(filename)) {
throw t('files', '{newName} already exists', {newName: filename}, undefined, {
escape: false
});
} else {
return true;
}
} catch (error) {
$input.attr('title', error);
$input.tooltip({placement: 'right', trigger: 'manual', 'container': '.newFileMenu'});
$input.tooltip('fixTitle');
$input.tooltip('show');
$input.addClass('error');
}
return false;
};
// verify filename on typing
$input.keyup(function() {
if (checkInput()) {
$input.tooltip('hide');
$input.removeClass('error');
}
});
$submit.click(function(event) {
event.stopPropagation();
event.preventDefault();
$form.submit();
});
$input.focus();
// pre select name up to the extension
lastPos = newName.lastIndexOf('.');
if (lastPos === -1) {
lastPos = newName.length;
}
$input.selectRange(0, lastPos);
$form.submit(function(event) {
event.stopPropagation();
event.preventDefault();
if (checkInput()) {
var newname = $input.val().trim();
/* Find the right actionHandler that should be called.
* Actions is retrieved by using `actionSpec.id` */
var action = _.filter(self._menuItems, function(item) {
return item.id == $target.attr('data-action');
}).pop();
action.actionHandler(newname);
$form.remove();
$target.find('.displayname').removeClass('hidden');
OC.hideMenus();
}
});
},
/**
* Add a new item menu entry in the “New” file menu (in
* last position). By clicking on the item, the
* `actionHandler` function is called.
*
* @param {Object} actionSpec item’s properties
*/
addMenuEntry: function(actionSpec) {
this._menuItems.push({
id: actionSpec.id,
displayName: actionSpec.displayName,
templateName: actionSpec.templateName,
iconClass: actionSpec.iconClass,
fileType: actionSpec.fileType,
actionHandler: actionSpec.actionHandler,
});
},
/**
* Renders the menu with the currently set items
*/
render: function() {
this.$el.html(this.template({
uploadMaxHumanFileSize: 'TODO',
uploadLabel: t('files', 'Upload file'),
items: this._menuItems
}));
// Trigger upload action also with keyboard navigation on enter
this.$el.find('[for="file_upload_start"]').on('keyup', function(event) {
if (event.key === " " || event.key === "Enter") {
$('#file_upload_start').trigger('click');
}
});
},
/**
* Displays the menu under the given element
*
* @param {Object} $target target element
*/
showAt: function($target) {
this.render();
OC.showMenu(null, this.$el);
}
});
OCA.Files.NewFileMenu = NewFileMenu;
})();
/*
* jQuery File Upload Plugin 9.12.5
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2010, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* jshint nomen:false */
/* global define, require, window, document, location, Blob, FormData */
;(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define([
'jquery',
'jquery.ui.widget'
], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS:
factory(
require('jquery'),
require('./vendor/jquery.ui.widget')
);
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Detect file input support, based on
// http://viljamis.com/blog/2012/file-upload-support-on-mobile/
$.support.fileInput = !(new RegExp(
// Handle devices which give false positives for the feature detection:
'(Android (1\\.[0156]|2\\.[01]))' +
'|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
'|(w(eb)?OSBrowser)|(webOS)' +
'|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
).test(window.navigator.userAgent) ||
// Feature detection for all other devices:
$('<input type="file">').prop('disabled'));
// The FileReader API is not actually used, but works as feature detection,
// as some Safari versions (5?) support XHR file uploads via the FormData API,
// but not non-multipart XHR file uploads.
// window.XMLHttpRequestUpload is not available on IE10, so we check for
// window.ProgressEvent instead to detect XHR2 file upload capability:
$.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
$.support.xhrFormDataFileUpload = !!window.FormData;
// Detect support for Blob slicing (required for chunked uploads):
$.support.blobSlice = window.Blob && (Blob.prototype.slice ||
Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
// Helper function to create drag handlers for dragover/dragenter/dragleave:
function getDragHandler(type) {
var isDragOver = type === 'dragover';
return function (e) {
e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
var dataTransfer = e.dataTransfer;
if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
this._trigger(
type,
$.Event(type, {delegatedEvent: e})
) !== false) {
e.preventDefault();
if (isDragOver) {
dataTransfer.dropEffect = 'copy';
}
}
};
}
// The fileupload widget listens for change events on file input fields defined
// via fileInput setting and paste or drop events of the given dropZone.
// In addition to the default jQuery Widget methods, the fileupload widget
// exposes the "add" and "send" methods, to add or directly send files using
// the fileupload API.
// By default, files added via file input selection, paste, drag & drop or
// "add" method are uploaded immediately, but it is possible to override
// the "add" callback option to queue file uploads.
$.widget('blueimp.fileupload', {
options: {
// The drop target element(s), by the default the complete document.
// Set to null to disable drag & drop support:
dropZone: $(document),
// The paste target element(s), by the default undefined.
// Set to a DOM node or jQuery object to enable file pasting:
pasteZone: undefined,
// The file input field(s), that are listened to for change events.
// If undefined, it is set to the file input fields inside
// of the widget element on plugin initialization.
// Set to null to disable the change listener.
fileInput: undefined,
// By default, the file input field is replaced with a clone after
// each input field change event. This is required for iframe transport
// queues and allows change events to be fired for the same file
// selection, but can be disabled by setting the following option to false:
replaceFileInput: true,
// The parameter name for the file form data (the request argument name).
// If undefined or empty, the name property of the file input field is
// used, or "files[]" if the file input name property is also empty,
// can be a string or an array of strings:
paramName: undefined,
// By default, each file of a selection is uploaded using an individual
// request for XHR type uploads. Set to false to upload file
// selections in one request each:
singleFileUploads: true,
// To limit the number of files uploaded with one XHR request,
// set the following option to an integer greater than 0:
limitMultiFileUploads: undefined,
// The following option limits the number of files uploaded with one
// XHR request to keep the request size under or equal to the defined
// limit in bytes:
limitMultiFileUploadSize: undefined,
// Multipart file uploads add a number of bytes to each uploaded file,
// therefore the following option adds an overhead for each file used
// in the limitMultiFileUploadSize configuration:
limitMultiFileUploadSizeOverhead: 512,
// Set the following option to true to issue all file upload requests
// in a sequential order:
sequentialUploads: false,
// To limit the number of concurrent uploads,
// set the following option to an integer greater than 0:
limitConcurrentUploads: undefined,
// Set the following option to true to force iframe transport uploads:
forceIframeTransport: false,
// Set the following option to the location of a redirect url on the
// origin server, for cross-domain iframe transport uploads:
redirect: undefined,
// The parameter name for the redirect url, sent as part of the form
// data and set to 'redirect' if this option is empty:
redirectParamName: undefined,
// Set the following option to the location of a postMessage window,
// to enable postMessage transport uploads:
postMessage: undefined,
// By default, XHR file uploads are sent as multipart/form-data.
// The iframe transport is always using multipart/form-data.
// Set to false to enable non-multipart XHR uploads:
multipart: true,
// To upload large files in smaller chunks, set the following option
// to a preferred maximum chunk size. If set to 0, null or undefined,
// or the browser does not support the required Blob API, files will
// be uploaded as a whole.
maxChunkSize: undefined,
// When a non-multipart upload or a chunked multipart upload has been
// aborted, this option can be used to resume the upload by setting
// it to the size of the already uploaded bytes. This option is most
// useful when modifying the options object inside of the "add" or
// "send" callbacks, as the options are cloned for each file upload.
uploadedBytes: undefined,
// By default, failed (abort or error) file uploads are removed from the
// global progress calculation. Set the following option to false to
// prevent recalculating the global progress data:
recalculateProgress: true,
// Interval in milliseconds to calculate and trigger progress events:
progressInterval: 100,
// Interval in milliseconds to calculate progress bitrate:
bitrateInterval: 500,
// By default, uploads are started automatically when adding files:
autoUpload: true,
// Error and info messages:
messages: {
uploadedBytes: 'Uploaded bytes exceed file size'
},
// Translation function, gets the message key to be translated
// and an object with context specific data as arguments:
i18n: function (message, context) {
message = this.messages[message] || message.toString();
if (context) {
$.each(context, function (key, value) {
message = message.replace('{' + key + '}', value);
});
}
return message;
},
// Additional form data to be sent along with the file uploads can be set
// using this option, which accepts an array of objects with name and
// value properties, a function returning such an array, a FormData
// object (for XHR file uploads), or a simple object.
// The form of the first fileInput is given as parameter to the function:
formData: function (form) {
return form.serializeArray();
},
// The add callback is invoked as soon as files are added to the fileupload
// widget (via file input selection, drag & drop, paste or add API call).
// If the singleFileUploads option is enabled, this callback will be
// called once for each file in the selection for XHR file uploads, else
// once for each file selection.
//
// The upload starts when the submit method is invoked on the data parameter.
// The data object contains a files property holding the added files
// and allows you to override plugin options as well as define ajax settings.
//
// Listeners for this callback can also be bound the following way:
// .bind('fileuploadadd', func);
//
// data.submit() returns a Promise object and allows to attach additional
// handlers using jQuery's Deferred callbacks:
// data.submit().done(func).fail(func).always(func);
add: function (e, data) {
if (e.isDefaultPrevented()) {
return false;
}
if (data.autoUpload || (data.autoUpload !== false &&
$(this).fileupload('option', 'autoUpload'))) {
data.process().done(function () {
data.submit();
});
}
},
// Other callbacks:
// Callback for the submit event of each file upload:
// submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
// Callback for the start of each file upload request:
// send: function (e, data) {}, // .bind('fileuploadsend', func);
// Callback for successful uploads:
// done: function (e, data) {}, // .bind('fileuploaddone', func);
// Callback for failed (abort or error) uploads:
// fail: function (e, data) {}, // .bind('fileuploadfail', func);
// Callback for completed (success, abort or error) requests:
// always: function (e, data) {}, // .bind('fileuploadalways', func);
// Callback for upload progress events:
// progress: function (e, data) {}, // .bind('fileuploadprogress', func);
// Callback for global upload progress events:
// progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
// Callback for uploads start, equivalent to the global ajaxStart event:
// start: function (e) {}, // .bind('fileuploadstart', func);
// Callback for uploads stop, equivalent to the global ajaxStop event:
// stop: function (e) {}, // .bind('fileuploadstop', func);
// Callback for change events of the fileInput(s):
// change: function (e, data) {}, // .bind('fileuploadchange', func);
// Callback for paste events to the pasteZone(s):
// paste: function (e, data) {}, // .bind('fileuploadpaste', func);
// Callback for drop events of the dropZone(s):
// drop: function (e, data) {}, // .bind('fileuploaddrop', func);
// Callback for drop events of the dropZone(s) when there are no files:
// dropnofiles: function (e) {}, // .bind('fileuploaddropnofiles', func);
// Callback for dragover events of the dropZone(s):
// dragover: function (e) {}, // .bind('fileuploaddragover', func);
// Callback for the start of each chunk upload request:
// chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
// Callback for successful chunk uploads:
// chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
// Callback for failed (abort or error) chunk uploads:
// chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
// Callback for completed (success, abort or error) chunk upload requests:
// chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
// The plugin options are used as settings object for the ajax calls.
// The following are jQuery ajax settings required for the file uploads:
processData: false,
contentType: false,
cache: false,
timeout: 0
},
// A list of options that require reinitializing event listeners and/or
// special initialization code:
_specialOptions: [
'fileInput',
'dropZone',
'pasteZone',
'multipart',
'forceIframeTransport'
],
_blobSlice: $.support.blobSlice && function () {
var slice = this.slice || this.webkitSlice || this.mozSlice;
return slice.apply(this, arguments);
},
_BitrateTimer: function () {
this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
this.loaded = 0;
this.bitrate = 0;
this.getBitrate = function (now, loaded, interval) {
var timeDiff = now - this.timestamp;
if (!this.bitrate || !interval || timeDiff > interval) {
this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
this.loaded = loaded;
this.timestamp = now;
}
return this.bitrate;
};
},
_isXHRUpload: function (options) {
return !options.forceIframeTransport &&
((!options.multipart && $.support.xhrFileUpload) ||
$.support.xhrFormDataFileUpload);
},
_getFormData: function (options) {
var formData;
if ($.type(options.formData) === 'function') {
return options.formData(options.form);
}
if ($.isArray(options.formData)) {
return options.formData;
}
if ($.type(options.formData) === 'object') {
formData = [];
$.each(options.formData, function (name, value) {
formData.push({name: name, value: value});
});
return formData;
}
return [];
},
_getTotal: function (files) {
var total = 0;
$.each(files, function (index, file) {
total += file.size || 1;
});
return total;
},
_initProgressObject: function (obj) {
var progress = {
loaded: 0,
total: 0,
bitrate: 0
};
if (obj._progress) {
$.extend(obj._progress, progress);
} else {
obj._progress = progress;
}
},
_initResponseObject: function (obj) {
var prop;
if (obj._response) {
for (prop in obj._response) {
if (obj._response.hasOwnProperty(prop)) {
delete obj._response[prop];
}
}
} else {
obj._response = {};
}
},
_onProgress: function (e, data) {
if (e.lengthComputable) {
var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
loaded;
if (data._time && data.progressInterval &&
(now - data._time < data.progressInterval) &&
e.loaded !== e.total) {
return;
}
data._time = now;
loaded = Math.floor(
e.loaded / e.total * (data.chunkSize || data._progress.total)
) + (data.uploadedBytes || 0);
// Add the difference from the previously loaded state
// to the global loaded counter:
this._progress.loaded += (loaded - data._progress.loaded);
this._progress.bitrate = this._bitrateTimer.getBitrate(
now,
this._progress.loaded,
data.bitrateInterval
);
data._progress.loaded = data.loaded = loaded;
data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
now,
loaded,
data.bitrateInterval
);
// Trigger a custom progress event with a total data property set
// to the file size(s) of the current upload and a loaded data
// property calculated accordingly:
this._trigger(
'progress',
$.Event('progress', {delegatedEvent: e}),
data
);
// Trigger a global progress event for all current file uploads,
// including ajax calls queued for sequential file uploads:
this._trigger(
'progressall',
$.Event('progressall', {delegatedEvent: e}),
this._progress
);
}
},
_initProgressListener: function (options) {
var that = this,
xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
// Accesss to the native XHR object is required to add event listeners
// for the upload progress event:
if (xhr.upload) {
$(xhr.upload).bind('progress', function (e) {
var oe = e.originalEvent;
// Make sure the progress event properties get copied over:
e.lengthComputable = oe.lengthComputable;
e.loaded = oe.loaded;
e.total = oe.total;
that._onProgress(e, options);
});
options.xhr = function () {
return xhr;
};
}
},
_isInstanceOf: function (type, obj) {
// Cross-frame instanceof check
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
},
_initXHRData: function (options) {
var that = this,
formData,
file = options.files[0],
// Ignore non-multipart setting if not supported:
multipart = options.multipart || !$.support.xhrFileUpload,
paramName = $.type(options.paramName) === 'array' ?
options.paramName[0] : options.paramName;
options.headers = $.extend({}, options.headers);
if (options.contentRange) {
options.headers['Content-Range'] = options.contentRange;
}
if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
options.headers['Content-Disposition'] = 'attachment; filename="' +
encodeURI(file.name) + '"';
}
if (!multipart) {
options.contentType = file.type || 'application/octet-stream';
options.data = options.blob || file;
} else if ($.support.xhrFormDataFileUpload) {
if (options.postMessage) {
// window.postMessage does not allow sending FormData
// objects, so we just add the File/Blob objects to
// the formData array and let the postMessage window
// create the FormData object out of this array:
formData = this._getFormData(options);
if (options.blob) {
formData.push({
name: paramName,
value: options.blob
});
} else {
$.each(options.files, function (index, file) {
formData.push({
name: ($.type(options.paramName) === 'array' &&
options.paramName[index]) || paramName,
value: file
});
});
}
} else {
if (that._isInstanceOf('FormData', options.formData)) {
formData = options.formData;
} else {
formData = new FormData();
$.each(this._getFormData(options), function (index, field) {
formData.append(field.name, field.value);
});
}
if (options.blob) {
formData.append(paramName, options.blob, file.name);
} else {
$.each(options.files, function (index, file) {
// This check allows the tests to run with
// dummy objects:
if (that._isInstanceOf('File', file) ||
that._isInstanceOf('Blob', file)) {
formData.append(
($.type(options.paramName) === 'array' &&
options.paramName[index]) || paramName,
file,
file.uploadName || file.name
);
}
});
}
}
options.data = formData;
}
// Blob reference is not needed anymore, free memory:
options.blob = null;
},
_initIframeSettings: function (options) {
var targetHost = $('<a></a>').prop('href', options.url).prop('host');
// Setting the dataType to iframe enables the iframe transport:
options.dataType = 'iframe ' + (options.dataType || '');
// The iframe transport accepts a serialized array as form data:
options.formData = this._getFormData(options);
// Add redirect url to form data on cross-domain uploads:
if (options.redirect && targetHost && targetHost !== location.host) {
options.formData.push({
name: options.redirectParamName || 'redirect',
value: options.redirect
});
}
},
_initDataSettings: function (options) {
if (this._isXHRUpload(options)) {
if (!this._chunkedUpload(options, true)) {
if (!options.data) {
this._initXHRData(options);
}
this._initProgressListener(options);
}
if (options.postMessage) {
// Setting the dataType to postmessage enables the
// postMessage transport:
options.dataType = 'postmessage ' + (options.dataType || '');
}
} else {
this._initIframeSettings(options);
}
},
_getParamName: function (options) {
var fileInput = $(options.fileInput),
paramName = options.paramName;
if (!paramName) {
paramName = [];
fileInput.each(function () {
var input = $(this),
name = input.prop('name') || 'files[]',
i = (input.prop('files') || [1]).length;
while (i) {
paramName.push(name);
i -= 1;
}
});
if (!paramName.length) {
paramName = [fileInput.prop('name') || 'files[]'];
}
} else if (!$.isArray(paramName)) {
paramName = [paramName];
}
return paramName;
},
_initFormSettings: function (options) {
// Retrieve missing options from the input field and the
// associated form, if available:
if (!options.form || !options.form.length) {
options.form = $(options.fileInput.prop('form'));
// If the given file input doesn't have an associated form,
// use the default widget file input's form:
if (!options.form.length) {
options.form = $(this.options.fileInput.prop('form'));
}
}
options.paramName = this._getParamName(options);
if (!options.url) {
options.url = options.form.prop('action') || location.href;
}
// The HTTP request method must be "POST" or "PUT":
options.type = (options.type ||
($.type(options.form.prop('method')) === 'string' &&
options.form.prop('method')) || ''
).toUpperCase();
if (options.type !== 'POST' && options.type !== 'PUT' &&
options.type !== 'PATCH') {
options.type = 'POST';
}
if (!options.formAcceptCharset) {
options.formAcceptCharset = options.form.attr('accept-charset');
}
},
_getAJAXSettings: function (data) {
var options = $.extend({}, this.options, data);
this._initFormSettings(options);
this._initDataSettings(options);
return options;
},
// jQuery 1.6 doesn't provide .state(),
// while jQuery 1.8+ removed .isRejected() and .isResolved():
_getDeferredState: function (deferred) {
if (deferred.state) {
return deferred.state();
}
if (deferred.isResolved()) {
return 'resolved';
}
if (deferred.isRejected()) {
return 'rejected';
}
return 'pending';
},
// Maps jqXHR callbacks to the equivalent
// methods of the given Promise object:
_enhancePromise: function (promise) {
promise.success = promise.done;
promise.error = promise.fail;
promise.complete = promise.always;
return promise;
},
// Creates and returns a Promise object enhanced with
// the jqXHR methods abort, success, error and complete:
_getXHRPromise: function (resolveOrReject, context, args) {
var dfd = $.Deferred(),
promise = dfd.promise();
context = context || this.options.context || promise;
if (resolveOrReject === true) {
dfd.resolveWith(context, args);
} else if (resolveOrReject === false) {
dfd.rejectWith(context, args);
}
promise.abort = dfd.promise;
return this._enhancePromise(promise);
},
// Adds convenience methods to the data callback argument:
_addConvenienceMethods: function (e, data) {
var that = this,
getPromise = function (args) {
return $.Deferred().resolveWith(that, args).promise();
};
data.process = function (resolveFunc, rejectFunc) {
if (resolveFunc || rejectFunc) {
data._processQueue = this._processQueue =
(this._processQueue || getPromise([this])).then(
function () {
if (data.errorThrown) {
return $.Deferred()
.rejectWith(that, [data]).promise();
}
return getPromise(arguments);
}
).then(resolveFunc, rejectFunc);
}
return this._processQueue || getPromise([this]);
};
data.submit = function () {
if (this.state() !== 'pending') {
data.jqXHR = this.jqXHR =
(that._trigger(
'submit',
$.Event('submit', {delegatedEvent: e}),
this
) !== false) && that._onSend(e, this);
}
return this.jqXHR || that._getXHRPromise();
};
data.abort = function () {
if (this.jqXHR) {
return this.jqXHR.abort();
}
this.errorThrown = 'abort';
that._trigger('fail', null, this);
return that._getXHRPromise(false);
};
data.state = function () {
if (this.jqXHR) {
return that._getDeferredState(this.jqXHR);
}
if (this._processQueue) {
return that._getDeferredState(this._processQueue);
}
};
data.processing = function () {
return !this.jqXHR && this._processQueue && that
._getDeferredState(this._processQueue) === 'pending';
};
data.progress = function () {
return this._progress;
};
data.response = function () {
return this._response;
};
},
// Parses the Range header from the server response
// and returns the uploaded bytes:
_getUploadedBytes: function (jqXHR) {
var range = jqXHR.getResponseHeader('Range'),
parts = range && range.split('-'),
upperBytesPos = parts && parts.length > 1 &&
parseInt(parts[1], 10);
return upperBytesPos && upperBytesPos + 1;
},
// Uploads a file in multiple, sequential requests
// by splitting the file up in multiple blob chunks.
// If the second parameter is true, only tests if the file
// should be uploaded in chunks, but does not invoke any
// upload requests:
_chunkedUpload: function (options, testOnly) {
options.uploadedBytes = options.uploadedBytes || 0;
var that = this,
file = options.files[0],
fs = file.size,
ub = options.uploadedBytes,
mcs = options.maxChunkSize || fs,
slice = this._blobSlice,
dfd = $.Deferred(),
promise = dfd.promise(),
jqXHR,
upload;
if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
options.data) {
return false;
}
if (testOnly) {
return true;
}
if (ub >= fs) {
file.error = options.i18n('uploadedBytes');
return this._getXHRPromise(
false,
options.context,
[null, 'error', file.error]
);
}
// The chunk upload method:
upload = function () {
// Clone the options object for each chunk upload:
var o = $.extend({}, options),
currentLoaded = o._progress.loaded;
o.blob = slice.call(
file,
ub,
ub + mcs,
file.type
);
// Store the current chunk size, as the blob itself
// will be dereferenced after data processing:
o.chunkSize = o.blob.size;
// Expose the chunk bytes position range:
o.contentRange = 'bytes ' + ub + '-' +
(ub + o.chunkSize - 1) + '/' + fs;
// Process the upload data (the blob and potential form data):
that._initXHRData(o);
// Add progress listeners for this chunk upload:
that._initProgressListener(o);
jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
that._getXHRPromise(false, o.context))
.done(function (result, textStatus, jqXHR) {
ub = that._getUploadedBytes(jqXHR) ||
(ub + o.chunkSize);
// Create a progress event if no final progress event
// with loaded equaling total has been triggered
// for this chunk:
if (currentLoaded + o.chunkSize - o._progress.loaded) {
that._onProgress($.Event('progress', {
lengthComputable: true,
loaded: ub - o.uploadedBytes,
total: ub - o.uploadedBytes
}), o);
}
options.uploadedBytes = o.uploadedBytes = ub;
o.result = result;
o.textStatus = textStatus;
o.jqXHR = jqXHR;
that._trigger('chunkdone', null, o);
that._trigger('chunkalways', null, o);
if (ub < fs) {
// File upload not yet complete,
// continue with the next chunk:
upload();
} else {
dfd.resolveWith(
o.context,
[result, textStatus, jqXHR]
);
}
})
.fail(function (jqXHR, textStatus, errorThrown) {
o.jqXHR = jqXHR;
o.textStatus = textStatus;
o.errorThrown = errorThrown;
that._trigger('chunkfail', null, o);
that._trigger('chunkalways', null, o);
dfd.rejectWith(
o.context,
[jqXHR, textStatus, errorThrown]
);
});
};
this._enhancePromise(promise);
promise.abort = function () {
return jqXHR.abort();
};
upload();
return promise;
},
_beforeSend: function (e, data) {
if (this._active === 0) {
// the start callback is triggered when an upload starts
// and no other uploads are currently running,
// equivalent to the global ajaxStart event:
this._trigger('start');
// Set timer for global bitrate progress calculation:
this._bitrateTimer = new this._BitrateTimer();
// Reset the global progress values:
this._progress.loaded = this._progress.total = 0;
this._progress.bitrate = 0;
}
// Make sure the container objects for the .response() and
// .progress() methods on the data object are available
// and reset to their initial state:
this._initResponseObject(data);
this._initProgressObject(data);
data._progress.loaded = data.loaded = data.uploadedBytes || 0;
data._progress.total = data.total = this._getTotal(data.files) || 1;
data._progress.bitrate = data.bitrate = 0;
this._active += 1;
// Initialize the global progress values:
this._progress.loaded += data.loaded;
this._progress.total += data.total;
},
_onDone: function (result, textStatus, jqXHR, options) {
var total = options._progress.total,
response = options._response;
if (options._progress.loaded < total) {
// Create a progress event if no final progress event
// with loaded equaling total has been triggered:
this._onProgress($.Event('progress', {
lengthComputable: true,
loaded: total,
total: total
}), options);
}
response.result = options.result = result;
response.textStatus = options.textStatus = textStatus;
response.jqXHR = options.jqXHR = jqXHR;
this._trigger('done', null, options);
},
_onFail: function (jqXHR, textStatus, errorThrown, options) {
var response = options._response;
if (options.recalculateProgress) {
// Remove the failed (error or abort) file upload from
// the global progress calculation:
this._progress.loaded -= options._progress.loaded;
this._progress.total -= options._progress.total;
}
response.jqXHR = options.jqXHR = jqXHR;
response.textStatus = options.textStatus = textStatus;
response.errorThrown = options.errorThrown = errorThrown;
this._trigger('fail', null, options);
},
_onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
// jqXHRorResult, textStatus and jqXHRorError are added to the
// options object via done and fail callbacks
this._trigger('always', null, options);
},
_onSend: function (e, data) {
if (!data.submit) {
this._addConvenienceMethods(e, data);
}
var that = this,
jqXHR,
aborted,
slot,
pipe,
options = that._getAJAXSettings(data),
send = function () {
that._sending += 1;
// Set timer for bitrate progress calculation:
options._bitrateTimer = new that._BitrateTimer();
jqXHR = jqXHR || (
((aborted || that._trigger(
'send',
$.Event('send', {delegatedEvent: e}),
options
) === false) &&
that._getXHRPromise(false, options.context, aborted)) ||
that._chunkedUpload(options) || $.ajax(options)
).done(function (result, textStatus, jqXHR) {
that._onDone(result, textStatus, jqXHR, options);
}).fail(function (jqXHR, textStatus, errorThrown) {
that._onFail(jqXHR, textStatus, errorThrown, options);
}).always(function (jqXHRorResult, textStatus, jqXHRorError) {
that._onAlways(
jqXHRorResult,
textStatus,
jqXHRorError,
options
);
that._sending -= 1;
that._active -= 1;
if (options.limitConcurrentUploads &&
options.limitConcurrentUploads > that._sending) {
// Start the next queued upload,
// that has not been aborted:
var nextSlot = that._slots.shift();
while (nextSlot) {
if (that._getDeferredState(nextSlot) === 'pending') {
nextSlot.resolve();
break;
}
nextSlot = that._slots.shift();
}
}
if (that._active === 0) {
// The stop callback is triggered when all uploads have
// been completed, equivalent to the global ajaxStop event:
that._trigger('stop');
}
});
return jqXHR;
};
this._beforeSend(e, options);
if (this.options.sequentialUploads ||
(this.options.limitConcurrentUploads &&
this.options.limitConcurrentUploads <= this._sending)) {
if (this.options.limitConcurrentUploads > 1) {
slot = $.Deferred();
this._slots.push(slot);
pipe = slot.then(send);
} else {
this._sequence = this._sequence.then(send, send);
pipe = this._sequence;
}
// Return the piped Promise object, enhanced with an abort method,
// which is delegated to the jqXHR object of the current upload,
// and jqXHR callbacks mapped to the equivalent Promise methods:
pipe.abort = function () {
aborted = [undefined, 'abort', 'abort'];
if (!jqXHR) {
if (slot) {
slot.rejectWith(options.context, aborted);
}
return send();
}
return jqXHR.abort();
};
return this._enhancePromise(pipe);
}
return send();
},
_onAdd: function (e, data) {
var that = this,
result = true,
options = $.extend({}, this.options, data),
files = data.files,
filesLength = files.length,
limit = options.limitMultiFileUploads,
limitSize = options.limitMultiFileUploadSize,
overhead = options.limitMultiFileUploadSizeOverhead,
batchSize = 0,
paramName = this._getParamName(options),
paramNameSet,
paramNameSlice,
fileSet,
i,
j = 0;
if (!filesLength) {
return false;
}
if (limitSize && files[0].size === undefined) {
limitSize = undefined;
}
if (!(options.singleFileUploads || limit || limitSize) ||
!this._isXHRUpload(options)) {
fileSet = [files];
paramNameSet = [paramName];
} else if (!(options.singleFileUploads || limitSize) && limit) {
fileSet = [];
paramNameSet = [];
for (i = 0; i < filesLength; i += limit) {
fileSet.push(files.slice(i, i + limit));
paramNameSlice = paramName.slice(i, i + limit);
if (!paramNameSlice.length) {
paramNameSlice = paramName;
}
paramNameSet.push(paramNameSlice);
}
} else if (!options.singleFileUploads && limitSize) {
fileSet = [];
paramNameSet = [];
for (i = 0; i < filesLength; i = i + 1) {
batchSize += files[i].size + overhead;
if (i + 1 === filesLength ||
((batchSize + files[i + 1].size + overhead) > limitSize) ||
(limit && i + 1 - j >= limit)) {
fileSet.push(files.slice(j, i + 1));
paramNameSlice = paramName.slice(j, i + 1);
if (!paramNameSlice.length) {
paramNameSlice = paramName;
}
paramNameSet.push(paramNameSlice);
j = i + 1;
batchSize = 0;
}
}
} else {
paramNameSet = paramName;
}
data.originalFiles = files;
$.each(fileSet || files, function (index, element) {
var newData = $.extend({}, data);
newData.files = fileSet ? element : [element];
newData.paramName = paramNameSet[index];
that._initResponseObject(newData);
that._initProgressObject(newData);
that._addConvenienceMethods(e, newData);
result = that._trigger(
'add',
$.Event('add', {delegatedEvent: e}),
newData
);
return result;
});
return result;
},
_replaceFileInput: function (data) {
var input = data.fileInput,
inputClone = input.clone(true),
restoreFocus = input.is(document.activeElement);
// Add a reference for the new cloned file input to the data argument:
data.fileInputClone = inputClone;
$('<form></form>').append(inputClone)[0].reset();
// Detaching allows to insert the fileInput on another form
// without loosing the file input value:
input.after(inputClone).detach();
// If the fileInput had focus before it was detached,
// restore focus to the inputClone.
if (restoreFocus) {
inputClone.focus();
}
// Avoid memory leaks with the detached file input:
$.cleanData(input.unbind('remove'));
// Replace the original file input element in the fileInput
// elements set with the clone, which has been copied including
// event handlers:
this.options.fileInput = this.options.fileInput.map(function (i, el) {
if (el === input[0]) {
return inputClone[0];
}
return el;
});
// If the widget has been initialized on the file input itself,
// override this.element with the file input clone:
if (input[0] === this.element[0]) {
this.element = inputClone;
}
},
_handleFileTreeEntry: function (entry, path) {
var that = this,
dfd = $.Deferred(),
errorHandler = function (e) {
if (e && !e.entry) {
e.entry = entry;
}
// Since $.when returns immediately if one
// Deferred is rejected, we use resolve instead.
// This allows valid files and invalid items
// to be returned together in one set:
dfd.resolve([e]);
},
successHandler = function (entries) {
that._handleFileTreeEntries(
entries,
path + entry.name + '/'
).done(function (files) {
dfd.resolve(files);
}).fail(errorHandler);
},
readEntries = function () {
dirReader.readEntries(function (results) {
if (!results.length) {
successHandler(entries);
} else {
entries = entries.concat(results);
readEntries();
}
}, errorHandler);
},
dirReader, entries = [];
path = path || '';
if (entry.isFile) {
if (entry._file) {
// Workaround for Chrome bug #149735
entry._file.relativePath = path;
dfd.resolve(entry._file);
} else {
entry.file(function (file) {
file.relativePath = path;
dfd.resolve(file);
}, errorHandler);
}
} else if (entry.isDirectory) {
dirReader = entry.createReader();
readEntries();
} else {
// Return an empy list for file system items
// other than files or directories:
dfd.resolve([]);
}
return dfd.promise();
},
_handleFileTreeEntries: function (entries, path) {
var that = this;
return $.when.apply(
$,
$.map(entries, function (entry) {
return that._handleFileTreeEntry(entry, path);
})
).then(function () {
return Array.prototype.concat.apply(
[],
arguments
);
});
},
_getDroppedFiles: function (dataTransfer) {
dataTransfer = dataTransfer || {};
var items = dataTransfer.items;
if (items && items.length && (items[0].webkitGetAsEntry ||
items[0].getAsEntry)) {
return this._handleFileTreeEntries(
$.map(items, function (item) {
var entry;
if (item.webkitGetAsEntry) {
entry = item.webkitGetAsEntry();
if (entry) {
// Workaround for Chrome bug #149735:
entry._file = item.getAsFile();
}
return entry;
}
return item.getAsEntry();
})
);
}
return $.Deferred().resolve(
$.makeArray(dataTransfer.files)
).promise();
},
_getSingleFileInputFiles: function (fileInput) {
fileInput = $(fileInput);
var entries = fileInput.prop('webkitEntries') ||
fileInput.prop('entries'),
files,
value;
if (entries && entries.length) {
return this._handleFileTreeEntries(entries);
}
files = $.makeArray(fileInput.prop('files'));
if (!files.length) {
value = fileInput.prop('value');
if (!value) {
return $.Deferred().resolve([]).promise();
}
// If the files property is not available, the browser does not
// support the File API and we add a pseudo File object with
// the input value as name with path information removed:
files = [{name: value.replace(/^.*\\/, '')}];
} else if (files[0].name === undefined && files[0].fileName) {
// File normalization for Safari 4 and Firefox 3:
$.each(files, function (index, file) {
file.name = file.fileName;
file.size = file.fileSize;
});
}
return $.Deferred().resolve(files).promise();
},
_getFileInputFiles: function (fileInput) {
if (!(fileInput instanceof $) || fileInput.length === 1) {
return this._getSingleFileInputFiles(fileInput);
}
return $.when.apply(
$,
$.map(fileInput, this._getSingleFileInputFiles)
).then(function () {
return Array.prototype.concat.apply(
[],
arguments
);
});
},
_onChange: function (e) {
var that = this,
data = {
fileInput: $(e.target),
form: $(e.target.form)
};
this._getFileInputFiles(data.fileInput).always(function (files) {
data.files = files;
if (that.options.replaceFileInput) {
that._replaceFileInput(data);
}
if (that._trigger(
'change',
$.Event('change', {delegatedEvent: e}),
data
) !== false) {
that._onAdd(e, data);
}
});
},
_onPaste: function (e) {
var items = e.originalEvent && e.originalEvent.clipboardData &&
e.originalEvent.clipboardData.items,
data = {files: []};
if (items && items.length) {
$.each(items, function (index, item) {
var file = item.getAsFile && item.getAsFile();
if (file) {
data.files.push(file);
}
});
if (this._trigger(
'paste',
$.Event('paste', {delegatedEvent: e}),
data
) !== false) {
this._onAdd(e, data);
}
}
},
_onDrop: function (e) {
e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
var that = this,
dataTransfer = e.dataTransfer,
data = {};
if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
e.preventDefault();
this._getDroppedFiles(dataTransfer).always(function (files) {
data.files = files;
if (that._trigger(
'drop',
$.Event('drop', {delegatedEvent: e}),
data
) !== false) {
that._onAdd(e, data);
}
});
} else {
// "dropnofiles" is triggered to allow proper cleanup of the
// drag and drop operation, as some browsers trigger "drop"
// events that have no files even if the "DataTransfer.types" of
// the "dragover" event included a "Files" item.
this._trigger(
'dropnofiles',
$.Event('drop', {delegatedEvent: e})
);
}
},
_onDragOver: getDragHandler('dragover'),
_onDragEnter: getDragHandler('dragenter'),
_onDragLeave: getDragHandler('dragleave'),
_initEventHandlers: function () {
if (this._isXHRUpload(this.options)) {
this._on(this.options.dropZone, {
dragover: this._onDragOver,
drop: this._onDrop,
// event.preventDefault() on dragenter is required for IE10+:
dragenter: this._onDragEnter,
// dragleave is not required, but added for completeness:
dragleave: this._onDragLeave
});
this._on(this.options.pasteZone, {
paste: this._onPaste
});
}
if ($.support.fileInput) {
this._on(this.options.fileInput, {
change: this._onChange
});
}
},
_destroyEventHandlers: function () {
this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
this._off(this.options.pasteZone, 'paste');
this._off(this.options.fileInput, 'change');
},
_setOption: function (key, value) {
var reinit = $.inArray(key, this._specialOptions) !== -1;
if (reinit) {
this._destroyEventHandlers();
}
this._super(key, value);
if (reinit) {
this._initSpecialOptions();
this._initEventHandlers();
}
},
_initSpecialOptions: function () {
var options = this.options;
if (options.fileInput === undefined) {
options.fileInput = this.element.is('input[type="file"]') ?
this.element : this.element.find('input[type="file"]');
} else if (!(options.fileInput instanceof $)) {
options.fileInput = $(options.fileInput);
}
if (!(options.dropZone instanceof $)) {
options.dropZone = $(options.dropZone);
}
if (!(options.pasteZone instanceof $)) {
options.pasteZone = $(options.pasteZone);
}
},
_getRegExp: function (str) {
var parts = str.split('/'),
modifiers = parts.pop();
parts.shift();
return new RegExp(parts.join('/'), modifiers);
},
_isRegExpOption: function (key, value) {
return key !== 'url' && $.type(value) === 'string' &&
/^\/.*\/[igm]{0,3}$/.test(value);
},
_initDataAttributes: function () {
var that = this,
options = this.options,
data = this.element.data();
// Initialize options set via HTML5 data-attributes:
$.each(
this.element[0].attributes,
function (index, attr) {
var key = attr.name.toLowerCase(),
value;
if (/^data-/.test(key)) {
// Convert hyphen-ated key to camelCase:
key = key.slice(5).replace(/-[a-z]/g, function (str) {
return str.charAt(1).toUpperCase();
});
value = data[key];
if (that._isRegExpOption(key, value)) {
value = that._getRegExp(value);
}
options[key] = value;
}
}
);
},
_create: function () {
this._initDataAttributes();
this._initSpecialOptions();
this._slots = [];
this._sequence = this._getXHRPromise(true);
this._sending = this._active = 0;
this._initProgressObject(this);
this._initEventHandlers();
},
// This method is exposed to the widget API and allows to query
// the number of active uploads:
active: function () {
return this._active;
},
// This method is exposed to the widget API and allows to query
// the widget upload progress.
// It returns an object with loaded, total and bitrate properties
// for the running uploads:
progress: function () {
return this._progress;
},
// This method is exposed to the widget API and allows adding files
// using the fileupload API. The data parameter accepts an object which
// must have a files property and can contain additional options:
// .fileupload('add', {files: filesList});
add: function (data) {
var that = this;
if (!data || this.options.disabled) {
return;
}
if (data.fileInput && !data.files) {
this._getFileInputFiles(data.fileInput).always(function (files) {
data.files = files;
that._onAdd(null, data);
});
} else {
data.files = $.makeArray(data.files);
this._onAdd(null, data);
}
},
// This method is exposed to the widget API and allows sending files
// using the fileupload API. The data parameter accepts an object which
// must have a files or fileInput property and can contain additional options:
// .fileupload('send', {files: filesList});
// The method returns a Promise object for the file upload call.
send: function (data) {
if (data && !this.options.disabled) {
if (data.fileInput && !data.files) {
var that = this,
dfd = $.Deferred(),
promise = dfd.promise(),
jqXHR,
aborted;
promise.abort = function () {
aborted = true;
if (jqXHR) {
return jqXHR.abort();
}
dfd.reject(null, 'abort', 'abort');
return promise;
};
this._getFileInputFiles(data.fileInput).always(
function (files) {
if (aborted) {
return;
}
if (!files.length) {
dfd.reject();
return;
}
data.files = files;
jqXHR = that._onSend(null, data);
jqXHR.then(
function (result, textStatus, jqXHR) {
dfd.resolve(result, textStatus, jqXHR);
},
function (jqXHR, textStatus, errorThrown) {
dfd.reject(jqXHR, textStatus, errorThrown);
}
);
}
);
return this._enhancePromise(promise);
}
data.files = $.makeArray(data.files);
if (data.files.length) {
return this._onSend(null, data);
}
}
return this._getXHRPromise(false, data && data.context);
}
});
}));
/*!
* jquery-visibility v1.0.11
* Page visibility shim for jQuery.
*
* Project Website: http://mths.be/visibility
*
* @version 1.0.11
* @license MIT.
* @author Mathias Bynens - @mathias
* @author Jan Paepke - @janpaepke
*/
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], function ($) {
return factory(root, $);
});
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(root, require('jquery'));
} else {
// Browser globals
factory(root, jQuery);
}
}(this, function(window, $, undefined) {
"use strict";
var
document = window.document,
property, // property name of document, that stores page visibility
vendorPrefixes = ['webkit', 'o', 'ms', 'moz', ''],
$support = $.support || {},
// In Opera, `'onfocusin' in document == true`, hence the extra `hasFocus` check to detect IE-like behavior
eventName = 'onfocusin' in document && 'hasFocus' in document ?
'focusin focusout' :
'focus blur';
var prefix;
while ((prefix = vendorPrefixes.pop()) !== undefined) {
property = (prefix ? prefix + 'H': 'h') + 'idden';
$support.pageVisibility = document[property] !== undefined;
if ($support.pageVisibility) {
eventName = prefix + 'visibilitychange';
break;
}
}
// normalize to and update document hidden property
function updateState() {
if (property !== 'hidden') {
document.hidden = $support.pageVisibility ? document[property] : undefined;
}
}
updateState();
$(/blur$/.test(eventName) ? window : document).on(eventName, function(event) {
var type = event.type;
var originalEvent = event.originalEvent;
// Avoid errors from triggered native events for which `originalEvent` is
// not available.
if (!originalEvent) {
return;
}
var toElement = originalEvent.toElement;
// If it’s a `{focusin,focusout}` event (IE), `fromElement` and `toElement`
// should both be `null` or `undefined`; else, the page visibility hasn’t
// changed, but the user just clicked somewhere in the doc. In IE9, we need
// to check the `relatedTarget` property instead.
if (
!/^focus./.test(type) || (
toElement === undefined &&
originalEvent.fromElement === undefined &&
originalEvent.relatedTarget === undefined
)
) {
$(document).triggerHandler(
property && document[property] || /^(?:blur|focusout)$/.test(type) ?
'hide' :
'show'
);
}
// and update the current state
updateState();
});
}));
/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function(OC, OCA) {
/**
* @class OC.Files.FileInfo
* @classdesc File information
*
* @param {Object} attributes file data
* @param {int} attributes.id file id
* @param {string} attributes.name file name
* @param {string} attributes.path path leading to the file,
* without the file name and with a leading slash
* @param {int} attributes.size size
* @param {string} attributes.mimetype mime type
* @param {string} attributes.icon icon URL
* @param {int} attributes.permissions permissions
* @param {Date} attributes.mtime modification time
* @param {string} attributes.etag etag
* @param {string} mountType mount type
*
* @since 8.2
*/
var FileInfoModel = OC.Backbone.Model.extend({
defaults: {
mimetype: 'application/octet-stream',
path: ''
},
_filesClient: null,
initialize: function(data, options) {
if (!_.isUndefined(data.id)) {
data.id = parseInt(data.id, 10);
}
if( options ){
if (options.filesClient) {
this._filesClient = options.filesClient;
}
}
},
/**
* Returns whether this file is a directory
*
* @return {boolean} true if this is a directory, false otherwise
*/
isDirectory: function() {
return this.get('mimetype') === 'httpd/unix-directory';
},
/**
* Returns whether this file is an image
*
* @return {boolean} true if this is an image, false otherwise
*/
isImage: function() {
if (!this.has('mimetype')) {
return false;
}
return this.get('mimetype').substr(0, 6) === 'image/'
|| this.get('mimetype') === 'application/postscript'
|| this.get('mimetype') === 'application/illustrator'
|| this.get('mimetype') === 'application/x-photoshop';
},
/**
* Returns the full path to this file
*
* @return {string} full path
*/
getFullPath: function() {
return OC.joinPaths(this.get('path'), this.get('name'));
},
/**
* Reloads missing properties from server and set them in the model.
* @param properties array of properties to be reloaded
* @return ajax call object
*/
reloadProperties: function(properties) {
if( !this._filesClient ){
return;
}
var self = this;
var deferred = $.Deferred();
var targetPath = OC.joinPaths(this.get('path') + '/', this.get('name'));
this._filesClient.getFileInfo(targetPath, {
properties: properties
})
.then(function(status, data) {
// the following lines should be extracted to a mapper
if( properties.indexOf(OC.Files.Client.PROPERTY_GETCONTENTLENGTH) !== -1
|| properties.indexOf(OC.Files.Client.PROPERTY_SIZE) !== -1 ) {
self.set('size', data.size);
}
deferred.resolve(status, data);
})
.fail(function(status) {
OC.Notification.show(t('files', 'Could not load info for file "{file}"', {file: self.get('name')}), {type: 'error'});
deferred.reject(status);
});
return deferred.promise();
}
});
if (!OCA.Files) {
OCA.Files = {};
}
OCA.Files.FileInfoModel = FileInfoModel;
})(OC, OCA);
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
(function() {
/**
* The FileSummary class encapsulates the file summary values and
* the logic to render it in the given container
*
* @constructs FileSummary
* @memberof OCA.Files
*
* @param $tr table row element
* @param {OC.Backbone.Model} [options.filesConfig] files app configuration
*/
var FileSummary = function($tr, options) {
options = options || {};
var self = this;
this.$el = $tr;
var filesConfig = options.config;
if (filesConfig) {
this._showHidden = !!filesConfig.get('showhidden');
filesConfig.on('change:showhidden', function() {
self._showHidden = !!this.get('showhidden');
self.update();
});
}
this.clear();
this.render();
};
FileSummary.prototype = {
_showHidden: null,
summary: {
totalFiles: 0,
totalDirs: 0,
totalHidden: 0,
totalSize: 0,
filter:'',
sumIsPending:false
},
/**
* Returns whether the given file info must be hidden
*
* @param {OC.Files.FileInfo} fileInfo file info
*
* @return {boolean} true if the file is a hidden file, false otherwise
*/
_isHiddenFile: function(file) {
return file.name && file.name.charAt(0) === '.';
},
/**
* Adds file
* @param {OC.Files.FileInfo} file file to add
* @param {boolean} update whether to update the display
*/
add: function(file, update) {
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
return;
}
if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
this.summary.totalDirs++;
}
else {
this.summary.totalFiles++;
}
if (this._isHiddenFile(file)) {
this.summary.totalHidden++;
}
var size = parseInt(file.size, 10) || 0;
if (size >=0) {
this.summary.totalSize += size;
} else {
this.summary.sumIsPending = true;
}
if (!!update) {
this.update();
}
},
/**
* Removes file
* @param {OC.Files.FileInfo} file file to remove
* @param {boolean} update whether to update the display
*/
remove: function(file, update) {
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
return;
}
if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
this.summary.totalDirs--;
}
else {
this.summary.totalFiles--;
}
if (this._isHiddenFile(file)) {
this.summary.totalHidden--;
}
var size = parseInt(file.size, 10) || 0;
if (size >=0) {
this.summary.totalSize -= size;
}
if (!!update) {
this.update();
}
},
setFilter: function(filter, files){
this.summary.filter = filter.toLowerCase();
this.calculate(files);
},
/**
* Returns the total of files and directories
*/
getTotal: function() {
return this.summary.totalDirs + this.summary.totalFiles;
},
/**
* Recalculates the summary based on the given files array
* @param files array of files
*/
calculate: function(files) {
var file;
var summary = {
totalDirs: 0,
totalFiles: 0,
totalHidden: 0,
totalSize: 0,
filter: this.summary.filter,
sumIsPending: false
};
for (var i = 0; i < files.length; i++) {
file = files[i];
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
continue;
}
if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
summary.totalDirs++;
}
else {
summary.totalFiles++;
}
if (this._isHiddenFile(file)) {
summary.totalHidden++;
}
var size = parseInt(file.size, 10) || 0;
if (size >=0) {
summary.totalSize += size;
} else {
summary.sumIsPending = true;
}
}
this.setSummary(summary);
},
/**
* Clears the summary
*/
clear: function() {
this.calculate([]);
},
/**
* Sets the current summary values
* @param summary map
*/
setSummary: function(summary) {
this.summary = summary;
if (typeof this.summary.filter === 'undefined') {
this.summary.filter = '';
}
this.update();
},
_infoTemplate: function(data) {
/* NOTE: To update the template make changes in filesummary.handlebars
* and run:
*
* handlebars -n OCA.Files.FileSummary.Templates filesummary.handlebars -f filesummary_template.js
*/
return OCA.Files.Templates['filesummary'](_.extend({
connectorLabel: t('files', '{dirs} and {files}', {dirs: '', files: ''})
}, data));
},
/**
* Renders the file summary element
*/
update: function() {
if (!this.$el) {
return;
}
if (!this.summary.totalFiles && !this.summary.totalDirs) {
this.$el.addClass('hidden');
return;
}
// There's a summary and data -> Update the summary
this.$el.removeClass('hidden');
var $dirInfo = this.$el.find('.dirinfo');
var $fileInfo = this.$el.find('.fileinfo');
var $connector = this.$el.find('.connector');
var $filterInfo = this.$el.find('.filter');
var $hiddenInfo = this.$el.find('.hiddeninfo');
// Substitute old content with new translations
$dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs));
$fileInfo.html(n('files', '%n file', '%n files', this.summary.totalFiles));
$hiddenInfo.html(' (' + n('files', 'including %n hidden', 'including %n hidden', this.summary.totalHidden) + ')');
var fileSize = this.summary.sumIsPending ? t('files', 'Pending') : OC.Util.humanFileSize(this.summary.totalSize);
this.$el.find('.filesize').html(fileSize);
// Show only what's necessary (may be hidden)
if (this.summary.totalDirs === 0) {
$dirInfo.addClass('hidden');
$connector.addClass('hidden');
} else {
$dirInfo.removeClass('hidden');
}
if (this.summary.totalFiles === 0) {
$fileInfo.addClass('hidden');
$connector.addClass('hidden');
} else {
$fileInfo.removeClass('hidden');
}
if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) {
$connector.removeClass('hidden');
}
$hiddenInfo.toggleClass('hidden', this.summary.totalHidden === 0 || this._showHidden)
if (this.summary.filter === '') {
$filterInfo.html('');
$filterInfo.addClass('hidden');
} else {
$filterInfo.html(' ' + n('files', 'matches \'{filter}\'', 'match \'{filter}\'', this.summary.totalDirs + this.summary.totalFiles, {filter: this.summary.filter}));
$filterInfo.removeClass('hidden');
}
},
render: function() {
if (!this.$el) {
return;
}
var summary = this.summary;
// don't show the filesize column, if filesize is NaN (e.g. in trashbin)
var fileSize = '';
if (!isNaN(summary.totalSize)) {
fileSize = summary.sumIsPending ? t('files', 'Pending') : OC.Util.humanFileSize(summary.totalSize);
fileSize = '<td class="filesize">' + fileSize + '</td>';
}
var $summary = $(
'<td>' + this._infoTemplate() + '</td>' +
fileSize +
'<td class="date"></td>'
);
this.$el.addClass('hidden');
this.$el.append($summary);
this.update();
}
};
OCA.Files.FileSummary = FileSummary;
})();
/*
* Copyright (c) 2018
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
var FileMultiSelectMenu = OC.Backbone.View.extend({
tagName: 'div',
className: 'filesSelectMenu popovermenu bubble menu-center',
_scopes: null,
initialize: function(menuItems) {
this._scopes = menuItems;
},
events: {
'click a.action': '_onClickAction'
},
/**
* Renders the menu with the currently set items
*/
render: function() {
this.$el.html(OCA.Files.Templates['filemultiselectmenu']({
items: this._scopes
}));
},
/**
* Displays the menu under the given element
*
* @param {OCA.Files.FileActionContext} context context
* @param {Object} $trigger trigger element
*/
show: function(context) {
this._context = context;
this.render();
this.$el.removeClass('hidden');
if (window.innerWidth < 480) {
this.$el.removeClass('menu-center').addClass('menu-right');
} else {
this.$el.removeClass('menu-right').addClass('menu-center');
}
OC.showMenu(null, this.$el);
return false;
},
toggleItemVisibility: function (itemName, show) {
if (show) {
this.$el.find('.item-' + itemName).removeClass('hidden');
} else {
this.$el.find('.item-' + itemName).addClass('hidden');
}
},
updateItemText: function (itemName, translation) {
this.$el.find('.item-' + itemName).find('.label').text(translation);
},
toggleLoading: function (itemName, showLoading) {
var $actionElement = this.$el.find('.item-' + itemName);
if ($actionElement.length === 0) {
return;
}
var $icon = $actionElement.find('.icon');
if (showLoading) {
var $loadingIcon = $('<span class="icon icon-loading-small"></span>');
$icon.after($loadingIcon);
$icon.addClass('hidden');
$actionElement.addClass('disabled');
} else {
$actionElement.find('.icon-loading-small').remove();
$actionElement.find('.icon').removeClass('hidden');
$actionElement.removeClass('disabled');
}
},
isDisabled: function (itemName) {
var $actionElement = this.$el.find('.item-' + itemName);
return $actionElement.hasClass('disabled');
},
/**
* Event handler whenever an action has been clicked within the menu
*
* @param {Object} event event object
*/
_onClickAction: function (event) {
var $target = $(event.currentTarget);
if (!$target.hasClass('menuitem')) {
$target = $target.closest('.menuitem');
}
OC.hideMenus();
this._context.multiSelectMenuClick(event, $target.data('action'));
return false;
}
});
OCA.Files.FileMultiSelectMenu = FileMultiSelectMenu;
})(OC, OCA);
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
(function() {
/**
* @class BreadCrumb
* @memberof OCA.Files
* @classdesc Breadcrumbs that represent the current path.
*
* @param {Object} [options] options
* @param {Function} [options.onClick] click event handler
* @param {Function} [options.onDrop] drop event handler
* @param {Function} [options.getCrumbUrl] callback that returns
* the URL of a given breadcrumb
*/
var BreadCrumb = function(options){
this.$el = $('<div class="breadcrumb"></div>');
this.$menu = $('<div class="popovermenu menu-center"><ul></ul></div>');
this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)';
this.hiddenCrumbSelector = '.crumb.hidden:not(.crumbhome):not(.crumbmenu)';
options = options || {};
if (options.onClick) {
this.onClick = options.onClick;
}
if (options.onDrop) {
this.onDrop = options.onDrop;
this.onOver = options.onOver;
this.onOut = options.onOut;
}
if (options.getCrumbUrl) {
this.getCrumbUrl = options.getCrumbUrl;
}
this._detailViews = [];
};
/**
* @memberof OCA.Files
*/
BreadCrumb.prototype = {
$el: null,
dir: null,
dirInfo: null,
/**
* Total width of all breadcrumbs
* @type int
* @private
*/
totalWidth: 0,
breadcrumbs: [],
onClick: null,
onDrop: null,
onOver: null,
onOut: null,
/**
* Sets the directory to be displayed as breadcrumb.
* This will re-render the breadcrumb.
* @param dir path to be displayed as breadcrumb
*/
setDirectory: function(dir) {
dir = dir.replace(/\\/g, '/');
dir = dir || '/';
if (dir !== this.dir) {
this.dir = dir;
this.render();
}
},
setDirectoryInfo: function(dirInfo) {
if (dirInfo !== this.dirInfo) {
this.dirInfo = dirInfo;
this.render();
}
},
/**
* @param {Backbone.View} detailView
*/
addDetailView: function(detailView) {
this._detailViews.push(detailView);
},
/**
* Returns the full URL to the given directory
*
* @param {Object.<String, String>} part crumb data as map
* @param {int} index crumb index
* @return full URL
*/
getCrumbUrl: function(part, index) {
return '#';
},
/**
* Renders the breadcrumb elements
*/
render: function() {
// Menu is destroyed on every change, we need to init it
OC.unregisterMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
var parts = this._makeCrumbs(this.dir || '/');
var $crumb;
var $menuItem;
this.$el.empty();
this.breadcrumbs = [];
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
var $image;
var $link = $('<a></a>');
$crumb = $('<div class="crumb svg"></div>');
if(part.dir) {
$link.attr('href', this.getCrumbUrl(part, i));
}
if(part.name) {
$link.text(part.name);
}
$link.addClass(part.linkclass);
$crumb.append($link);
$crumb.data('dir', part.dir);
// Ignore menu button
$crumb.data('crumb-id', i - 1);
$crumb.addClass(part.class);
if (part.img) {
$image = $('<img class="svg"></img>');
$image.attr('src', part.img);
$image.attr('alt', part.alt);
$link.append($image);
}
this.breadcrumbs.push($crumb);
this.$el.append($crumb);
// Only add feedback if not menu
if (this.onClick && i !== 0) {
$link.on('click', this.onClick);
}
}
// Menu creation
this._createMenu();
for (var j = 0; j < parts.length; j++) {
var menuPart = parts[j];
if(menuPart.dir) {
$menuItem = $('<li class="crumblist"><a><span class="icon-folder"></span><span></span></a></li>');
$menuItem.data('dir', menuPart.dir);
$menuItem.find('a').attr('href', this.getCrumbUrl(part, j));
$menuItem.find('span:eq(1)').text(menuPart.name);
this.$menu.children('ul').append($menuItem);
if (this.onClick) {
$menuItem.on('click', this.onClick);
}
}
}
_.each(this._detailViews, function(view) {
view.render({
dirInfo: this.dirInfo
});
$crumb.append(view.$el);
}, this);
// setup drag and drop
if (this.onDrop) {
this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({
drop: this.onDrop,
over: this.onOver,
out: this.onOut,
tolerance: 'pointer',
hoverClass: 'canDrop',
greedy: true
});
}
// Menu is destroyed on every change, we need to init it
OC.registerMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
this._resize();
},
/**
* Makes a breadcrumb structure based on the given path
*
* @param {String} dir path to split into a breadcrumb structure
* @return {Object.<String, String>} map of {dir: path, name: displayName}
*/
_makeCrumbs: function(dir) {
var crumbs = [];
var pathToHere = '';
// trim leading and trailing slashes
dir = dir.replace(/^\/+|\/+$/g, '');
var parts = dir.split('/');
if (dir === '') {
parts = [];
}
// menu part
crumbs.push({
class: 'crumbmenu hidden',
linkclass: 'icon-more menutoggle'
});
// root part
crumbs.push({
name: t('core', 'Home'),
dir: '/',
class: 'crumbhome',
linkclass: 'icon-home'
});
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
pathToHere = pathToHere + '/' + part;
crumbs.push({
dir: pathToHere,
name: part
});
}
return crumbs;
},
/**
* Calculate real width based on individual crumbs
*
* @param {boolean} ignoreHidden ignore hidden crumbs
*/
getTotalWidth: function(ignoreHidden) {
// The width has to be calculated by adding up the width of all the
// crumbs; getting the width of the breadcrumb element is not a
// valid approach, as the returned value could be clamped to its
// parent width.
var totalWidth = 0;
for (var i = 0; i < this.breadcrumbs.length; i++ ) {
var $crumb = $(this.breadcrumbs[i]);
if(!$crumb.hasClass('hidden') || ignoreHidden === true) {
totalWidth += $crumb.outerWidth(true);
}
}
return totalWidth;
},
/**
* Hide the middle crumb
*/
_hideCrumb: function() {
var length = this.$el.find(this.crumbSelector).length;
// Get the middle one floored down
var elmt = Math.floor(length / 2 - 0.5);
this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden');
},
/**
* Get the crumb to show
*/
_getCrumbElement: function() {
var hidden = this.$el.find(this.hiddenCrumbSelector).length;
var shown = this.$el.find(this.crumbSelector).length;
// Get the outer one with priority to the highest
var elmt = (1 - shown % 2) * (hidden - 1);
return this.$el.find(this.hiddenCrumbSelector + ':eq('+elmt+')');
},
/**
* Show the middle crumb
*/
_showCrumb: function() {
if(this.$el.find(this.hiddenCrumbSelector).length === 1) {
this.$el.find(this.hiddenCrumbSelector).removeClass('hidden');
}
this._getCrumbElement().removeClass('hidden');
},
/**
* Create and append the popovermenu
*/
_createMenu: function() {
this.$el.find('.crumbmenu').append(this.$menu);
this.$menu.children('ul').empty();
},
/**
* Update the popovermenu
*/
_updateMenu: function() {
var menuItems = this.$el.find(this.hiddenCrumbSelector);
this.$menu.find('li').addClass('in-breadcrumb');
for (var i = 0; i < menuItems.length; i++) {
var crumbId = $(menuItems[i]).data('crumb-id');
this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb');
}
},
_resize: function() {
if (this.breadcrumbs.length <= 2) {
// home & menu
return;
}
// Always hide the menu to ensure that it does not interfere with
// the width calculations; otherwise, the result could be different
// depending on whether the menu was previously being shown or not.
this.$el.find('.crumbmenu').addClass('hidden');
// Show the crumbs to compress the siblings before hidding again the
// crumbs. This is needed when the siblings expand to fill all the
// available width, as in that case their old width would limit the
// available width for the crumbs.
// Note that the crumbs shown always overflow the parent width
// (except, of course, when they all fit in).
while (this.$el.find(this.hiddenCrumbSelector).length > 0
&& this.getTotalWidth() <= this.$el.parent().width()) {
this._showCrumb();
}
var siblingsWidth = 0;
this.$el.prevAll(':visible').each(function () {
siblingsWidth += $(this).outerWidth(true);
});
this.$el.nextAll(':visible').each(function () {
siblingsWidth += $(this).outerWidth(true);
});
var availableWidth = this.$el.parent().width() - siblingsWidth;
// If container is smaller than content
// AND if there are crumbs left to hide
while (this.getTotalWidth() > availableWidth
&& this.$el.find(this.crumbSelector).length > 0) {
// As soon as one of the crumbs is hidden the menu will be
// shown. This is needed for proper results in further width
// checks.
// Note that the menu is not shown only when all the crumbs were
// being shown and they all fit the available space; if any of
// the crumbs was not being shown then those shown would
// overflow the available width, so at least one will be hidden
// and thus the menu will be shown.
this.$el.find('.crumbmenu').removeClass('hidden');
this._hideCrumb();
}
this._updateMenu();
}
};
OCA.Files.BreadCrumb = BreadCrumb;
})();
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* @class OCA.Files.FileList
* @classdesc
*
* The FileList class manages a file list view.
* A file list view consists of a controls bar and
* a file list table.
*
* @param $el container element with existing markup for the #controls
* and a table
* @param {Object} [options] map of options, see other parameters
* @param {Object} [options.scrollContainer] scrollable container, defaults to $(window)
* @param {Object} [options.dragOptions] drag options, disabled by default
* @param {Object} [options.folderDropOptions] folder drop options, disabled by default
* @param {boolean} [options.detailsViewEnabled=true] whether to enable details view
* @param {boolean} [options.enableUpload=false] whether to enable uploader
* @param {OC.Files.Client} [options.filesClient] files client to use
*/
var FileList = function($el, options) {
this.initialize($el, options);
};
/**
* @memberof OCA.Files
*/
FileList.prototype = {
SORT_INDICATOR_ASC_CLASS: 'icon-triangle-n',
SORT_INDICATOR_DESC_CLASS: 'icon-triangle-s',
id: 'files',
appName: t('files', 'Files'),
isEmpty: true,
useUndo:true,
/**
* Top-level container with controls and file list
*/
$el: null,
/**
* Files table
*/
$table: null,
/**
* List of rows (table tbody)
*/
$fileList: null,
/**
* @type OCA.Files.BreadCrumb
*/
breadcrumb: null,
/**
* @type OCA.Files.FileSummary
*/
fileSummary: null,
/**
* @type OCA.Files.DetailsView
*/
_detailsView: null,
/**
* Files client instance
*
* @type OC.Files.Client
*/
filesClient: null,
/**
* Whether the file list was initialized already.
* @type boolean
*/
initialized: false,
/**
* Wheater the file list was already shown once
* @type boolean
*/
shown: false,
/**
* Number of files per page
* Always show a minimum of 1
*
* @return {int} page size
*/
pageSize: function() {
var isGridView = this.$showGridView.is(':checked');
var columns = 1;
var rows = Math.ceil(this.$container.height() / 50);
if (isGridView) {
columns = Math.ceil(this.$container.width() / 160);
rows = Math.ceil(this.$container.height() / 160);
}
return Math.max(columns*rows, columns);
},
/**
* Array of files in the current folder.
* The entries are of file data.
*
* @type Array.<OC.Files.FileInfo>
*/
files: [],
/**
* Current directory entry
*
* @type OC.Files.FileInfo
*/
dirInfo: null,
/**
* File actions handler, defaults to OCA.Files.FileActions
* @type OCA.Files.FileActions
*/
fileActions: null,
/**
* File selection menu, defaults to OCA.Files.FileSelectionMenu
* @type OCA.Files.FileSelectionMenu
*/
fileMultiSelectMenu: null,
/**
* Whether selection is allowed, checkboxes and selection overlay will
* be rendered
*/
_allowSelection: true,
/**
* Map of file id to file data
* @type Object.<int, Object>
*/
_selectedFiles: {},
/**
* Summary of selected files.
* @type OCA.Files.FileSummary
*/
_selectionSummary: null,
/**
* If not empty, only files containing this string will be shown
* @type String
*/
_filter: '',
/**
* @type Backbone.Model
*/
_filesConfig: undefined,
/**
* Sort attribute
* @type String
*/
_sort: 'name',
/**
* Sort direction: 'asc' or 'desc'
* @type String
*/
_sortDirection: 'asc',
/**
* Sort comparator function for the current sort
* @type Function
*/
_sortComparator: null,
/**
* Whether to do a client side sort.
* When false, clicking on a table header will call reload().
* When true, clicking on a table header will simply resort the list.
*/
_clientSideSort: true,
/**
* Whether or not users can change the sort attribute or direction
*/
_allowSorting: true,
/**
* Current directory
* @type String
*/
_currentDirectory: null,
_dragOptions: null,
_folderDropOptions: null,
/**
* @type OC.Uploader
*/
_uploader: null,
/**
* Initialize the file list and its components
*
* @param $el container element with existing markup for the #controls
* and a table
* @param options map of options, see other parameters
* @param options.scrollContainer scrollable container, defaults to $(window)
* @param options.dragOptions drag options, disabled by default
* @param options.folderDropOptions folder drop options, disabled by default
* @param options.scrollTo name of file to scroll to after the first load
* @param {OC.Files.Client} [options.filesClient] files API client
* @param {OC.Backbone.Model} [options.filesConfig] files app configuration
* @private
*/
initialize: function($el, options) {
var self = this;
options = options || {};
if (this.initialized) {
return;
}
if (options.shown) {
this.shown = options.shown;
}
if (options.config) {
this._filesConfig = options.config;
} else if (!_.isUndefined(OCA.Files) && !_.isUndefined(OCA.Files.App)) {
this._filesConfig = OCA.Files.App.getFilesConfig();
} else {
this._filesConfig = new OC.Backbone.Model({
'showhidden': false
});
}
if (options.dragOptions) {
this._dragOptions = options.dragOptions;
}
if (options.folderDropOptions) {
this._folderDropOptions = options.folderDropOptions;
}
if (options.filesClient) {
this.filesClient = options.filesClient;
} else {
// default client if not specified
this.filesClient = OC.Files.getClient();
}
this.$el = $el;
if (options.id) {
this.id = options.id;
}
this.$container = options.scrollContainer || $(window);
this.$table = $el.find('table:first');
this.$fileList = $el.find('#fileList');
if (!_.isUndefined(this._filesConfig)) {
this._filesConfig.on('change:showhidden', function() {
var showHidden = this.get('showhidden');
self.$el.toggleClass('hide-hidden-files', !showHidden);
self.updateSelectionSummary();
if (!showHidden) {
// hiding files could make the page too small, need to try rendering next page
self._onScroll();
}
});
this.$el.toggleClass('hide-hidden-files', !this._filesConfig.get('showhidden'));
}
if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
this._detailsView = new OCA.Files.DetailsView();
this._detailsView.$el.addClass('disappear');
}
this._initFileActions(options.fileActions);
if (this._detailsView) {
this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView({fileList: this, fileActions: this.fileActions}));
}
this.files = [];
this._selectedFiles = {};
this._selectionSummary = new OCA.Files.FileSummary(undefined, {config: this._filesConfig});
// dummy root dir info
this.dirInfo = new OC.Files.FileInfo({});
this.fileSummary = this._createSummary();
if (options.multiSelectMenu) {
this.multiSelectMenuItems = options.multiSelectMenu;
for (var i=0; i<this.multiSelectMenuItems.length; i++) {
if (_.isFunction(this.multiSelectMenuItems[i])) {
this.multiSelectMenuItems[i] = this.multiSelectMenuItems[i](this);
}
}
this.fileMultiSelectMenu = new OCA.Files.FileMultiSelectMenu(this.multiSelectMenuItems);
this.fileMultiSelectMenu.render();
this.$el.find('.selectedActions').append(this.fileMultiSelectMenu.$el);
}
if (options.sorting) {
this.setSort(options.sorting.mode, options.sorting.direction, false, false);
} else {
this.setSort('name', 'asc', false, false);
}
var breadcrumbOptions = {
onClick: _.bind(this._onClickBreadCrumb, this),
getCrumbUrl: function(part) {
return self.linkTo(part.dir);
}
};
// if dropping on folders is allowed, then also allow on breadcrumbs
if (this._folderDropOptions) {
breadcrumbOptions.onDrop = _.bind(this._onDropOnBreadCrumb, this);
breadcrumbOptions.onOver = function() {
self.$el.find('td.filename.ui-droppable').droppable('disable');
};
breadcrumbOptions.onOut = function() {
self.$el.find('td.filename.ui-droppable').droppable('enable');
};
}
this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions);
var $controls = this.$el.find('#controls');
if ($controls.length > 0) {
$controls.prepend(this.breadcrumb.$el);
this.$table.addClass('has-controls');
}
this._renderNewButton();
this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
// Toggle for grid view, only register once
this.$showGridView = $('input#showgridview:not(.registered)');
this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
this.$showGridView.addClass('registered');
$('#view-toggle').tooltip({placement: 'bottom', trigger: 'hover'});
this._onResize = _.debounce(_.bind(this._onResize, this), 250);
$('#app-content').on('appresized', this._onResize);
$(window).resize(this._onResize);
this.$el.on('show', this._onResize);
this.updateSearch();
this.$fileList.on('click','td.filename>a.name, td.filesize, td.date', _.bind(this._onClickFile, this));
this.$fileList.on("droppedOnFavorites", function (event, file) {
self.fileActions.triggerAction('Favorite', self.getModelForFile(file), self);
});
this.$fileList.on('droppedOnTrash', function (event, filename, directory) {
self.do_delete(filename, directory);
});
this.$fileList.on('change', 'td.selection>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
this.$fileList.on('mouseover', 'td.selection', _.bind(this._onMouseOverCheckbox, this));
this.$el.on('show', _.bind(this._onShow, this));
this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this));
this.$el.find('.actions-selected').click(function () {
self.fileMultiSelectMenu.show(self);
return false;
});
this.$container.on('scroll', _.bind(this._onScroll, this));
if (options.scrollTo) {
this.$fileList.one('updated', function() {
self.scrollTo(options.scrollTo);
});
}
this._operationProgressBar = new OCA.Files.OperationProgressBar();
this._operationProgressBar.render();
this.$el.find('#uploadprogresswrapper').replaceWith(this._operationProgressBar.$el);
if (options.enableUpload) {
// TODO: auto-create this element
var $uploadEl = this.$el.find('#file_upload_start');
if ($uploadEl.exists()) {
this._uploader = new OC.Uploader($uploadEl, {
progressBar: this._operationProgressBar,
fileList: this,
filesClient: this.filesClient,
dropZone: $('#content'),
maxChunkSize: options.maxChunkSize
});
this.setupUploadEvents(this._uploader);
}
}
OC.Plugins.attach('OCA.Files.FileList', this);
},
/**
* Destroy / uninitialize this instance.
*/
destroy: function() {
if (this._newFileMenu) {
this._newFileMenu.remove();
}
if (this._newButton) {
this._newButton.remove();
}
if (this._detailsView) {
this._detailsView.remove();
}
// TODO: also unregister other event handlers
this.fileActions.off('registerAction', this._onFileActionsUpdated);
this.fileActions.off('setDefault', this._onFileActionsUpdated);
OC.Plugins.detach('OCA.Files.FileList', this);
$('#app-content').off('appresized', this._onResize);
},
_selectionMode: 'single',
_getCurrentSelectionMode: function () {
return this._selectionMode;
},
_onClickToggleSelectionMode: function () {
this._selectionMode = (this._selectionMode === 'range') ? 'single' : 'range';
if (this._selectionMode === 'single') {
this._removeHalfSelection();
}
},
multiSelectMenuClick: function (ev, action) {
var actionFunction = _.find(this.multiSelectMenuItems, function (item) {return item.name === action;}).action;
if (actionFunction) {
actionFunction(ev);
return;
}
switch (action) {
case 'delete':
this._onClickDeleteSelected(ev)
break;
case 'download':
this._onClickDownloadSelected(ev);
break;
case 'copyMove':
this._onClickCopyMoveSelected(ev);
break;
case 'restore':
this._onClickRestoreSelected(ev);
break;
}
},
/**
* Initializes the file actions, set up listeners.
*
* @param {OCA.Files.FileActions} fileActions file actions
*/
_initFileActions: function(fileActions) {
var self = this;
this.fileActions = fileActions;
if (!this.fileActions) {
this.fileActions = new OCA.Files.FileActions();
this.fileActions.registerDefaultActions();
}
if (this._detailsView) {
this.fileActions.registerAction({
name: 'Details',
displayName: t('files', 'Details'),
mime: 'all',
order: -50,
iconClass: 'icon-details',
permissions: OC.PERMISSION_NONE,
actionHandler: function(fileName, context) {
self._updateDetailsView(fileName);
}
});
}
this._onFileActionsUpdated = _.debounce(_.bind(this._onFileActionsUpdated, this), 100);
this.fileActions.on('registerAction', this._onFileActionsUpdated);
this.fileActions.on('setDefault', this._onFileActionsUpdated);
},
/**
* Returns a unique model for the given file name.
*
* @param {string|object} fileName file name or jquery row
* @return {OCA.Files.FileInfoModel} file info model
*/
getModelForFile: function(fileName) {
var self = this;
var $tr;
// jQuery object ?
if (fileName.is) {
$tr = fileName;
fileName = $tr.attr('data-file');
} else {
$tr = this.findFileEl(fileName);
}
if (!$tr || !$tr.length) {
return null;
}
// if requesting the selected model, return it
if (this._currentFileModel && this._currentFileModel.get('name') === fileName) {
return this._currentFileModel;
}
// TODO: note, this is a temporary model required for synchronising
// state between different views.
// In the future the FileList should work with Backbone.Collection
// and contain existing models that can be used.
// This method would in the future simply retrieve the matching model from the collection.
var model = new OCA.Files.FileInfoModel(this.elementToFile($tr), {
filesClient: this.filesClient
});
if (!model.get('path')) {
model.set('path', this.getCurrentDirectory(), {silent: true});
}
model.on('change', function(model) {
// re-render row
var highlightState = $tr.hasClass('highlighted');
$tr = self.updateRow(
$tr,
model.toJSON(),
{updateSummary: true, silent: false, animate: true}
);
// restore selection state
var selected = !!self._selectedFiles[$tr.data('id')];
self._selectFileEl($tr, selected);
$tr.toggleClass('highlighted', highlightState);
});
model.on('busy', function(model, state) {
self.showFileBusyState($tr, state);
});
return model;
},
/**
* Displays the details view for the given file and
* selects the given tab
*
* @param {string|OCA.Files.FileInfoModel} fileName file name or FileInfoModel for which to show details
* @param {string} [tabId] optional tab id to select
*/
showDetailsView: function(fileName, tabId) {
this._updateDetailsView(fileName);
if (tabId) {
this._detailsView.selectTab(tabId);
}
OC.Apps.showAppSidebar(this._detailsView.$el);
},
/**
* Update the details view to display the given file
*
* @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object
* @param {boolean} [show=true] whether to open the sidebar if it was closed
*/
_updateDetailsView: function(fileName, show) {
if (!this._detailsView) {
return;
}
// show defaults to true
show = _.isUndefined(show) || !!show;
var oldFileInfo = this._detailsView.getFileInfo();
if (oldFileInfo) {
// TODO: use more efficient way, maybe track the highlight
this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted');
oldFileInfo.off('change', this._onSelectedModelChanged, this);
}
if (!fileName) {
this._detailsView.$el.find('[data-original-title]').tooltip('hide')
this._detailsView.setFileInfo(null);
if (this._currentFileModel) {
this._currentFileModel.off();
}
this._currentFileModel = null;
OC.Apps.hideAppSidebar(this._detailsView.$el);
return;
}
if (show && this._detailsView.$el.hasClass('disappear')) {
OC.Apps.showAppSidebar(this._detailsView.$el);
}
if (fileName instanceof OCA.Files.FileInfoModel) {
var model = fileName;
} else {
var $tr = this.findFileEl(fileName);
var model = this.getModelForFile($tr);
$tr.addClass('highlighted');
}
this._currentFileModel = model;
this._replaceDetailsViewElementIfNeeded();
this._detailsView.setFileInfo(model);
this._detailsView.$el.scrollTop(0);
},
/**
* Replaces the current details view element with the details view
* element of this file list.
*
* Each file list has its own DetailsView object, and each one has its
* own root element, but there can be just one details view/sidebar
* element in the document. This helper method replaces the current
* details view/sidebar element in the document with the element from
* the DetailsView object of this file list.
*/
_replaceDetailsViewElementIfNeeded: function() {
var $appSidebar = $('#app-sidebar');
if ($appSidebar.length === 0) {
this._detailsView.$el.insertAfter($('#app-content'));
} else if ($appSidebar[0] !== this._detailsView.el) {
// "replaceWith()" can not be used here, as it removes the old
// element instead of just detaching it.
this._detailsView.$el.insertBefore($appSidebar);
$appSidebar.detach();
}
},
/**
* Event handler for when the window size changed
*/
_onResize: function() {
var containerWidth = this.$el.width();
var actionsWidth = 0;
$.each(this.$el.find('#controls .actions'), function(index, action) {
actionsWidth += $(action).outerWidth();
});
this.breadcrumb._resize();
},
/**
* Toggle showing gridview by default or not
*
* @returns {undefined}
*/
_onGridviewChange: function() {
var show = this.$showGridView.is(':checked');
// only save state if user is logged in
if (OC.currentUser) {
$.post(OC.generateUrl('/apps/files/api/v1/showgridview'), {
show: show
});
}
this.$showGridView.next('#view-toggle')
.removeClass('icon-toggle-filelist icon-toggle-pictures')
.addClass(show ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
$('.list-container').toggleClass('view-grid', show);
if (show) {
// If switching into grid view from list view, too few files might be displayed
// Try rendering the next page
this._onScroll();
}
},
/**
* Event handler when leaving previously hidden state
*/
_onShow: function(e) {
if (this.shown) {
if (e.itemId === this.id) {
this._setCurrentDir('/', false);
}
// Only reload if we don't navigate to a different directory
if (typeof e.dir === 'undefined' || e.dir === this.getCurrentDirectory()) {
this.reload();
}
}
this.shown = true;
},
/**
* Event handler for when the URL changed
*/
_onUrlChanged: function(e) {
if (e && _.isString(e.dir)) {
var currentDir = this.getCurrentDirectory();
// this._currentDirectory is NULL when fileList is first initialised
if( (this._currentDirectory || this.$el.find('#dir').val()) && currentDir === e.dir) {
return;
}
this.changeDirectory(e.dir, false, true);
}
},
/**
* Selected/deselects the given file element and updated
* the internal selection cache.
*
* @param {Object} $tr single file row element
* @param {bool} state true to select, false to deselect
*/
_selectFileEl: function($tr, state) {
var $checkbox = $tr.find('td.selection>.selectCheckBox');
var oldData = !!this._selectedFiles[$tr.data('id')];
var data;
$checkbox.prop('checked', state);
$tr.toggleClass('selected', state);
// already selected ?
if (state === oldData) {
return;
}
data = this.elementToFile($tr);
if (state) {
this._selectedFiles[$tr.data('id')] = data;
this._selectionSummary.add(data);
}
else {
delete this._selectedFiles[$tr.data('id')];
this._selectionSummary.remove(data);
}
if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
// hide sidebar
this._updateDetailsView(null);
}
this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
},
_selectRange: function($tr) {
var checked = $tr.hasClass('selected');
var $lastTr = $(this._lastChecked);
var lastIndex = $lastTr.index();
var currentIndex = $tr.index();
var $rows = this.$fileList.children('tr');
// last clicked checkbox below current one ?
if (lastIndex > currentIndex) {
var aux = lastIndex;
lastIndex = currentIndex;
currentIndex = aux;
}
// auto-select everything in-between
for (var i = lastIndex; i <= currentIndex; i++) {
this._selectFileEl($rows.eq(i), !checked);
}
this._removeHalfSelection();
this._selectionMode = 'single';
},
_selectSingle: function($tr) {
var state = !$tr.hasClass('selected');
this._selectFileEl($tr, state);
},
_onMouseOverCheckbox: function(e) {
if (this._getCurrentSelectionMode() !== 'range') {
return;
}
var $currentTr = $(e.target).closest('tr');
var $lastTr = $(this._lastChecked);
var lastIndex = $lastTr.index();
var currentIndex = $currentTr.index();
var $rows = this.$fileList.children('tr');
// last clicked checkbox below current one ?
if (lastIndex > currentIndex) {
var aux = lastIndex;
lastIndex = currentIndex;
currentIndex = aux;
}
// auto-select everything in-between
this._removeHalfSelection();
for (var i = 0; i <= $rows.length; i++) {
var $tr = $rows.eq(i);
var $checkbox = $tr.find('td.selection>.selectCheckBox');
if(lastIndex <= i && i <= currentIndex) {
$tr.addClass('halfselected');
$checkbox.prop('checked', true);
}
}
},
_removeHalfSelection: function() {
var $rows = this.$fileList.children('tr');
for (var i = 0; i <= $rows.length; i++) {
var $tr = $rows.eq(i);
$tr.removeClass('halfselected');
var $checkbox = $tr.find('td.selection>.selectCheckBox');
$checkbox.prop('checked', !!this._selectedFiles[$tr.data('id')]);
}
},
/**
* Event handler for when clicking on files to select them
*/
_onClickFile: function(event) {
var $tr = $(event.target).closest('tr');
if ($tr.hasClass('dragging')) {
return;
}
if (this._allowSelection && (event.ctrlKey || event.shiftKey)) {
event.preventDefault();
if (event.shiftKey) {
this._selectRange($tr);
} else {
this._selectSingle($tr);
}
this._lastChecked = $tr;
this.updateSelectionSummary();
} else {
// clicked directly on the name
if (!this._detailsView || $(event.target).is('.nametext, .name, .thumbnail') || $(event.target).closest('.nametext').length) {
var filename = $tr.attr('data-file');
var renaming = $tr.data('renaming');
if (!renaming) {
this.fileActions.currentFile = $tr.find('td');
var mime = this.fileActions.getCurrentMimeType();
var type = this.fileActions.getCurrentType();
var permissions = this.fileActions.getCurrentPermissions();
var action = this.fileActions.getDefault(mime,type, permissions);
if (action) {
event.preventDefault();
// also set on global object for legacy apps
window.FileActions.currentFile = this.fileActions.currentFile;
action(filename, {
$file: $tr,
fileList: this,
fileActions: this.fileActions,
dir: $tr.attr('data-path') || this.getCurrentDirectory()
});
}
// deselect row
$(event.target).closest('a').blur();
}
} else {
// Even if there is no Details action the default event
// handler is prevented for consistency (although there
// should always be a Details action); otherwise the link
// would be downloaded by the browser when the user expected
// the details to be shown.
event.preventDefault();
var filename = $tr.attr('data-file');
this.fileActions.currentFile = $tr.find('td');
var mime = this.fileActions.getCurrentMimeType();
var type = this.fileActions.getCurrentType();
var permissions = this.fileActions.getCurrentPermissions();
var action = this.fileActions.get(mime, type, permissions)['Details'];
if (action) {
// also set on global object for legacy apps
window.FileActions.currentFile = this.fileActions.currentFile;
action(filename, {
$file: $tr,
fileList: this,
fileActions: this.fileActions,
dir: $tr.attr('data-path') || this.getCurrentDirectory()
});
}
}
}
},
/**
* Event handler for when clicking on a file's checkbox
*/
_onClickFileCheckbox: function(e) {
var $tr = $(e.target).closest('tr');
if(this._getCurrentSelectionMode() === 'range') {
this._selectRange($tr);
} else {
this._selectSingle($tr);
}
this._lastChecked = $tr;
this.updateSelectionSummary();
if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
// hide sidebar
this._updateDetailsView(null);
}
},
/**
* Event handler for when selecting/deselecting all files
*/
_onClickSelectAll: function(e) {
var hiddenFiles = this.$fileList.find('tr.hidden');
var checked = e.target.checked;
if (hiddenFiles.length > 0) {
// set indeterminate alongside checked
e.target.indeterminate = checked;
} else {
e.target.indeterminate = false
}
// Select only visible checkboxes to filter out unmatched file in search
this.$fileList.find('td.selection > .selectCheckBox:visible').prop('checked', checked)
.closest('tr').toggleClass('selected', checked);
if (checked) {
for (var i = 0; i < this.files.length; i++) {
// a search will automatically hide the unwanted rows
// let's only select the matches
var fileData = this.files[i];
var fileRow = this.$fileList.find('tr[data-id=' + fileData.id + ']');
// do not select already selected ones
if (!fileRow.hasClass('hidden') && _.isUndefined(this._selectedFiles[fileData.id])) {
this._selectedFiles[fileData.id] = fileData;
this._selectionSummary.add(fileData);
}
}
} else {
// if we have some hidden row, then we're in a search
// Let's only deselect the visible ones
if (hiddenFiles.length > 0) {
var visibleFiles = this.$fileList.find('tr:not(.hidden)');
var self = this;
visibleFiles.each(function() {
var id = parseInt($(this).data('id'));
// do not deselect already deselected ones
if (!_.isUndefined(self._selectedFiles[id])) {
// a search will automatically hide the unwanted rows
// let's only select the matches
var fileData = self._selectedFiles[id];
delete self._selectedFiles[fileData.id];
self._selectionSummary.remove(fileData);
}
});
} else {
this._selectedFiles = {};
this._selectionSummary.clear();
}
}
this.updateSelectionSummary();
if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
// hide sidebar
this._updateDetailsView(null);
}
},
/**
* Event handler for when clicking on "Download" for the selected files
*/
_onClickDownloadSelected: function(event) {
var files;
var self = this;
var dir = this.getCurrentDirectory();
if (this.isAllSelected() && this.getSelectedFiles().length > 1) {
files = OC.basename(dir);
dir = OC.dirname(dir) || '/';
}
else {
files = _.pluck(this.getSelectedFiles(), 'name');
}
// don't allow a second click on the download action
if(this.fileMultiSelectMenu.isDisabled('download')) {
return false;
}
this.fileMultiSelectMenu.toggleLoading('download', true);
var disableLoadingState = function(){
self.fileMultiSelectMenu.toggleLoading('download', false);
};
if(this.getSelectedFiles().length > 1) {
OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir, true), disableLoadingState);
}
else {
var first = this.getSelectedFiles()[0];
OCA.Files.Files.handleDownload(this.getDownloadUrl(first.name, dir, true), disableLoadingState);
}
event.preventDefault();
},
/**
* Event handler for when clicking on "Move" for the selected files
*/
_onClickCopyMoveSelected: function(event) {
var files;
var self = this;
files = _.pluck(this.getSelectedFiles(), 'name');
// don't allow a second click on the download action
if(this.fileMultiSelectMenu.isDisabled('copyMove')) {
return false;
}
var disableLoadingState = function(){
self.fileMultiSelectMenu.toggleLoading('copyMove', false);
};
var actions = this.isSelectedMovable() ? OC.dialogs.FILEPICKER_TYPE_COPY_MOVE : OC.dialogs.FILEPICKER_TYPE_COPY;
var dialogDir = self.getCurrentDirectory();
if (typeof self.dirInfo.dirLastCopiedTo !== 'undefined') {
dialogDir = self.dirInfo.dirLastCopiedTo;
}
OC.dialogs.filepicker(t('files', 'Choose target folder'), function(targetPath, type) {
self.fileMultiSelectMenu.toggleLoading('copyMove', true);
if (type === OC.dialogs.FILEPICKER_TYPE_COPY) {
self.copy(files, targetPath, disableLoadingState);
}
if (type === OC.dialogs.FILEPICKER_TYPE_MOVE) {
self.move(files, targetPath, disableLoadingState);
}
self.dirInfo.dirLastCopiedTo = targetPath;
}, false, "httpd/unix-directory", true, actions, dialogDir);
event.preventDefault();
},
/**
* Event handler for when clicking on "Delete" for the selected files
*/
_onClickDeleteSelected: function(event) {
var files = null;
if (!this.isAllSelected()) {
files = _.pluck(this.getSelectedFiles(), 'name');
}
this.do_delete(files);
event.preventDefault();
},
/**
* Event handler when clicking on a table header
*/
_onClickHeader: function(e) {
if (this.$table.hasClass('multiselect')) {
return;
}
var $target = $(e.target);
var sort;
if (!$target.is('a')) {
$target = $target.closest('a');
}
sort = $target.attr('data-sort');
if (sort && this._allowSorting) {
if (this._sort === sort) {
this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc', true, true);
}
else {
if ( sort === 'name' ) { //default sorting of name is opposite to size and mtime
this.setSort(sort, 'asc', true, true);
}
else {
this.setSort(sort, 'desc', true, true);
}
}
}
},
/**
* Event handler when clicking on a bread crumb
*/
_onClickBreadCrumb: function(e) {
// Select a crumb or a crumb in the menu
var $el = $(e.target).closest('.crumb, .crumblist'),
$targetDir = $el.data('dir');
if ($targetDir !== undefined && e.which === 1) {
e.preventDefault();
this.changeDirectory($targetDir, true, true);
this.updateSearch();
}
},
/**
* Event handler for when scrolling the list container.
* This appends/renders the next page of entries when reaching the bottom.
*/
_onScroll: function(e) {
if (this.$container.scrollTop() + this.$container.height() > this.$el.height() - 300) {
this._nextPage(true);
}
},
/**
* Event handler when dropping on a breadcrumb
*/
_onDropOnBreadCrumb: function( event, ui ) {
var self = this;
var $target = $(event.target);
if (!$target.is('.crumb, .crumblist')) {
$target = $target.closest('.crumb, .crumblist');
}
var targetPath = $(event.target).data('dir');
var dir = this.getCurrentDirectory();
while (dir.substr(0,1) === '/') {//remove extra leading /'s
dir = dir.substr(1);
}
dir = '/' + dir;
if (dir.substr(-1,1) !== '/') {
dir = dir + '/';
}
// do nothing if dragged on current dir
if (targetPath === dir || targetPath + '/' === dir) {
return;
}
var files = this.getSelectedFiles();
if (files.length === 0) {
// single one selected without checkbox?
files = _.map(ui.helper.find('tr'), function(el) {
return self.elementToFile($(el));
});
}
var movePromise = this.move(_.pluck(files, 'name'), targetPath);
// re-enable td elements to be droppable
// sometimes the filename drop handler is still called after re-enable,
// it seems that waiting for a short time before re-enabling solves the problem
setTimeout(function() {
self.$el.find('td.filename.ui-droppable').droppable('enable');
}, 10);
return movePromise;
},
/**
* Sets a new page title
*/
setPageTitle: function(title){
if (title) {
title += ' - ';
} else {
title = '';
}
title += this.appName;
// Sets the page title with the " - Nextcloud" suffix as in templates
window.document.title = title + ' - ' + OC.theme.title;
return true;
},
/**
* Returns the file info for the given file name from the internal collection.
*
* @param {string} fileName file name
* @return {OCA.Files.FileInfo} file info or null if it was not found
*
* @since 8.2
*/
findFile: function(fileName) {
return _.find(this.files, function(aFile) {
return (aFile.name === fileName);
}) || null;
},
/**
* Returns the tr element for a given file name, but only if it was already rendered.
*
* @param {string} fileName file name
* @return {Object} jQuery object of the matching row
*/
findFileEl: function(fileName){
// use filterAttr to avoid escaping issues
return this.$fileList.find('tr').filterAttr('data-file', fileName);
},
/**
* Returns the file data from a given file element.
* @param $el file tr element
* @return file data
*/
elementToFile: function($el){
$el = $($el);
var data = {
id: parseInt($el.attr('data-id'), 10),
name: $el.attr('data-file'),
mimetype: $el.attr('data-mime'),
mtime: parseInt($el.attr('data-mtime'), 10),
type: $el.attr('data-type'),
etag: $el.attr('data-etag'),
permissions: parseInt($el.attr('data-permissions'), 10),
hasPreview: $el.attr('data-has-preview') === 'true',
isEncrypted: $el.attr('data-e2eencrypted') === 'true'
};
var size = $el.attr('data-size');
if (size) {
data.size = parseInt(size, 10);
}
var icon = $el.attr('data-icon');
if (icon) {
data.icon = icon;
}
var mountType = $el.attr('data-mounttype');
if (mountType) {
data.mountType = mountType;
}
var path = $el.attr('data-path');
if (path) {
data.path = path;
}
return data;
},
/**
* Appends the next page of files into the table
* @param animate true to animate the new elements
* @return array of DOM elements of the newly added files
*/
_nextPage: function(animate) {
var index = this.$fileList.children().length,
count = this.pageSize(),
hidden,
tr,
fileData,
newTrs = [],
isAllSelected = this.isAllSelected(),
showHidden = this._filesConfig.get('showhidden');
if (index >= this.files.length) {
return false;
}
while (count > 0 && index < this.files.length) {
fileData = this.files[index];
if (this._filter) {
hidden = fileData.name.toLowerCase().indexOf(this._filter.toLowerCase()) === -1;
} else {
hidden = false;
}
tr = this._renderRow(fileData, {updateSummary: false, silent: true, hidden: hidden});
this.$fileList.append(tr);
if (isAllSelected || this._selectedFiles[fileData.id]) {
tr.addClass('selected');
tr.find('.selectCheckBox').prop('checked', true);
}
if (animate) {
tr.addClass('appear transparent');
}
newTrs.push(tr);
index++;
// only count visible rows
if (showHidden || !tr.hasClass('hidden-file')) {
count--;
}
}
// trigger event for newly added rows
if (newTrs.length > 0) {
this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: newTrs}));
}
if (animate) {
// defer, for animation
window.setTimeout(function() {
for (var i = 0; i < newTrs.length; i++ ) {
newTrs[i].removeClass('transparent');
}
}, 0);
}
return newTrs;
},
/**
* Event handler for when file actions were updated.
* This will refresh the file actions on the list.
*/
_onFileActionsUpdated: function() {
var self = this;
var $files = this.$fileList.find('tr');
if (!$files.length) {
return;
}
$files.each(function() {
self.fileActions.display($(this).find('td.filename'), false, self);
});
this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $files}));
},
/**
* Sets the files to be displayed in the list.
* This operation will re-render the list and update the summary.
* @param filesArray array of file data (map)
*/
setFiles: function(filesArray) {
var self = this;
// detach to make adding multiple rows faster
this.files = filesArray;
this.$fileList.empty();
if (this._allowSelection) {
// The results table, which has no selection column, checks
// whether the main table has a selection column or not in order
// to align its contents with those of the main table.
this.$el.addClass('has-selection');
}
// clear "Select all" checkbox
this.$el.find('.select-all').prop('checked', false);
// Save full files list while rendering
this.isEmpty = this.files.length === 0;
this._nextPage();
this.updateEmptyContent();
this.fileSummary.calculate(this.files);
this._selectedFiles = {};
this._selectionSummary.clear();
this.updateSelectionSummary();
$(window).scrollTop(0);
this.$fileList.trigger(jQuery.Event('updated'));
_.defer(function() {
self.$el.closest('#app-content').trigger(jQuery.Event('apprendered'));
});
},
/**
* Returns whether the given file info must be hidden
*
* @param {OC.Files.FileInfo} fileInfo file info
*
* @return {boolean} true if the file is a hidden file, false otherwise
*/
_isHiddenFile: function(file) {
return file.name && file.name.charAt(0) === '.';
},
/**
* Returns the icon URL matching the given file info
*
* @param {OC.Files.FileInfo} fileInfo file info
*
* @return {string} icon URL
*/
_getIconUrl: function(fileInfo) {
var mimeType = fileInfo.mimetype || 'application/octet-stream';
if (mimeType === 'httpd/unix-directory') {
// use default folder icon
if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') {
return OC.MimeType.getIconUrl('dir-shared');
} else if (fileInfo.mountType === 'external-root') {
return OC.MimeType.getIconUrl('dir-external');
} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType);
}
return OC.MimeType.getIconUrl('dir');
}
return OC.MimeType.getIconUrl(mimeType);
},
/**
* Creates a new table row element using the given file data.
* @param {OC.Files.FileInfo} fileData file info attributes
* @param options map of attributes
* @return new tr element (not appended to the table)
*/
_createRow: function(fileData, options) {
var td, simpleSize, basename, extension, sizeColor,
icon = fileData.icon || this._getIconUrl(fileData),
name = fileData.name,
// TODO: get rid of type, only use mime type
type = fileData.type || 'file',
mtime = parseInt(fileData.mtime, 10),
mime = fileData.mimetype,
path = fileData.path,
dataIcon = null,
linkUrl;
options = options || {};
if (isNaN(mtime)) {
mtime = new Date().getTime();
}
if (type === 'dir') {
mime = mime || 'httpd/unix-directory';
if (fileData.isEncrypted) {
icon = OC.MimeType.getIconUrl('dir-encrypted');
dataIcon = icon;
} else if (fileData.mountType && fileData.mountType.indexOf('external') === 0) {
icon = OC.MimeType.getIconUrl('dir-external');
dataIcon = icon;
}
}
var permissions = fileData.permissions;
if (permissions === undefined || permissions === null) {
permissions = this.getDirectoryPermissions();
}
//containing tr
var tr = $('<tr></tr>').attr({
"data-id" : fileData.id,
"data-type": type,
"data-size": fileData.size,
"data-file": name,
"data-mime": mime,
"data-mtime": mtime,
"data-etag": fileData.etag,
"data-permissions": permissions,
"data-has-preview": fileData.hasPreview !== false,
"data-e2eencrypted": fileData.isEncrypted === true
});
if (dataIcon) {
// icon override
tr.attr('data-icon', dataIcon);
}
if (fileData.mountType) {
// dirInfo (parent) only exist for the "real" file list
if (this.dirInfo.id) {
// FIXME: HACK: detect shared-root
if (fileData.mountType === 'shared' && this.dirInfo.mountType !== 'shared' && this.dirInfo.mountType !== 'shared-root') {
// if parent folder isn't share, assume the displayed folder is a share root
fileData.mountType = 'shared-root';
} else if (fileData.mountType === 'external' && this.dirInfo.mountType !== 'external' && this.dirInfo.mountType !== 'external-root') {
// if parent folder isn't external, assume the displayed folder is the external storage root
fileData.mountType = 'external-root';
}
}
tr.attr('data-mounttype', fileData.mountType);
}
if (!_.isUndefined(path)) {
tr.attr('data-path', path);
}
else {
path = this.getCurrentDirectory();
}
// selection td
if (this._allowSelection) {
td = $('<td class="selection"></td>');
td.append(
'<input id="select-' + this.id + '-' + fileData.id +
'" type="checkbox" class="selectCheckBox checkbox"/><label for="select-' + this.id + '-' + fileData.id + '">' +
'<span class="hidden-visually">' + t('files', 'Select') + '</span>' +
'</label>'
);
tr.append(td);
}
// filename td
td = $('<td class="filename"></td>');
// linkUrl
if (mime === 'httpd/unix-directory') {
linkUrl = this.linkTo(path + '/' + name);
}
else {
linkUrl = this.getDownloadUrl(name, path, type === 'dir');
}
var linkElem = $('<a></a>').attr({
"class": "name",
"href": linkUrl
});
linkElem.append('<div class="thumbnail-wrapper"><div class="thumbnail" style="background-image:url(' + icon + ');"></div></div>');
// from here work on the display name
name = fileData.displayName || name;
// show hidden files (starting with a dot) completely in gray
if(name.indexOf('.') === 0) {
basename = '';
extension = name;
// split extension from filename for non dirs
} else if (mime !== 'httpd/unix-directory' && name.indexOf('.') !== -1) {
basename = name.substr(0, name.lastIndexOf('.'));
extension = name.substr(name.lastIndexOf('.'));
} else {
basename = name;
extension = false;
}
var nameSpan=$('<span></span>').addClass('nametext');
var innernameSpan = $('<span></span>').addClass('innernametext').text(basename);
var conflictingItems = this.$fileList.find('tr[data-file="' + this._jqSelEscape(name) + '"]');
if (conflictingItems.length !== 0) {
if (conflictingItems.length === 1) {
// Update the path on the first conflicting item
var $firstConflict = $(conflictingItems[0]),
firstConflictPath = $firstConflict.attr('data-path') + '/';
if (firstConflictPath.charAt(0) === '/') {
firstConflictPath = firstConflictPath.substr(1);
}
if (firstConflictPath && firstConflictPath !== '/') {
$firstConflict.find('td.filename span.innernametext').prepend($('<span></span>').addClass('conflict-path').text(firstConflictPath));
}
}
var conflictPath = path + '/';
if (conflictPath.charAt(0) === '/') {
conflictPath = conflictPath.substr(1);
}
if (path && path !== '/') {
nameSpan.append($('<span></span>').addClass('conflict-path').text(conflictPath));
}
}
nameSpan.append(innernameSpan);
linkElem.append(nameSpan);
if (extension) {
nameSpan.append($('<span></span>').addClass('extension').text(extension));
}
if (fileData.extraData) {
if (fileData.extraData.charAt(0) === '/') {
fileData.extraData = fileData.extraData.substr(1);
}
nameSpan.addClass('extra-data').attr('title', fileData.extraData);
nameSpan.tooltip({placement: 'top'});
}
// dirs can show the number of uploaded files
if (mime === 'httpd/unix-directory') {
linkElem.append($('<span></span>').attr({
'class': 'uploadtext',
'currentUploads': 0
}));
}
td.append(linkElem);
tr.append(td);
try {
var maxContrastHex = window.getComputedStyle(document.documentElement)
.getPropertyValue('--color-text-maxcontrast').trim()
if (maxContrastHex.length < 4) {
throw Error();
}
var maxContrast = parseInt(maxContrastHex.substring(1, 3), 16)
} catch(error) {
var maxContrast = OCA.Accessibility
&& OCA.Accessibility.theme === 'themedark'
? 130
: 118
}
// size column
if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) {
simpleSize = humanFileSize(parseInt(fileData.size, 10), true);
// rgb(118, 118, 118) / #767676
// min. color contrast for normal text on white background according to WCAG AA
sizeColor = Math.round(118-Math.pow((fileData.size/(1024*1024)), 2));
// ensure that the brightest color is still readable
// min. color contrast for normal text on white background according to WCAG AA
if (sizeColor >= maxContrast) {
sizeColor = maxContrast;
}
if (OCA.Accessibility && OCA.Accessibility.theme === 'themedark') {
sizeColor = Math.abs(sizeColor);
// ensure that the dimmest color is still readable
// min. color contrast for normal text on black background according to WCAG AA
if (sizeColor < maxContrast) {
sizeColor = maxContrast;
}
}
} else {
simpleSize = t('files', 'Pending');
}
td = $('<td></td>').attr({
"class": "filesize",
"style": 'color:rgb(' + sizeColor + ',' + sizeColor + ',' + sizeColor + ')'
}).text(simpleSize);
tr.append(td);
// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
// difference in days multiplied by 5 - brightest shade for files older than 32 days (160/5)
var modifiedColor = Math.round(((new Date()).getTime() - mtime )/1000/60/60/24*5 );
// ensure that the brightest color is still readable
// min. color contrast for normal text on white background according to WCAG AA
if (modifiedColor >= maxContrast) {
modifiedColor = maxContrast;
}
if (OCA.Accessibility && OCA.Accessibility.theme === 'themedark') {
modifiedColor = Math.abs(modifiedColor);
// ensure that the dimmest color is still readable
// min. color contrast for normal text on black background according to WCAG AA
if (modifiedColor < maxContrast) {
modifiedColor = maxContrast;
}
}
var formatted;
var text;
if (mtime > 0) {
formatted = OC.Util.formatDate(mtime);
text = OC.Util.relativeModifiedDate(mtime);
} else {
formatted = t('files', 'Unable to determine date');
text = '?';
}
td = $('<td></td>').attr({ "class": "date" });
td.append($('<span></span>').attr({
"class": "modified live-relative-timestamp",
"title": formatted,
"data-timestamp": mtime,
"style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')'
}).text(text)
.tooltip({placement: 'top'})
);
tr.find('.filesize').text(simpleSize);
tr.append(td);
return tr;
},
/* escape a selector expression for jQuery */
_jqSelEscape: function (expression) {
if (expression) {
return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
}
return null;
},
/**
* Adds an entry to the files array and also into the DOM
* in a sorted manner.
*
* @param {OC.Files.FileInfo} fileData map of file attributes
* @param {Object} [options] map of attributes
* @param {boolean} [options.updateSummary] true to update the summary
* after adding (default), false otherwise. Defaults to true.
* @param {boolean} [options.silent] true to prevent firing events like "fileActionsReady",
* defaults to false.
* @param {boolean} [options.animate] true to animate the thumbnail image after load
* defaults to true.
* @return new tr element (not appended to the table)
*/
add: function(fileData, options) {
var index;
var $tr;
var $rows;
var $insertionPoint;
options = _.extend({animate: true}, options || {});
// there are three situations to cover:
// 1) insertion point is visible on the current page
// 2) insertion point is on a not visible page (visible after scrolling)
// 3) insertion point is at the end of the list
$rows = this.$fileList.children();
index = this._findInsertionIndex(fileData);
if (index > this.files.length) {
index = this.files.length;
}
else {
$insertionPoint = $rows.eq(index);
}
// is the insertion point visible ?
if ($insertionPoint.length) {
// only render if it will really be inserted
$tr = this._renderRow(fileData, options);
$insertionPoint.before($tr);
}
else {
// if insertion point is after the last visible
// entry, append
if (index === $rows.length) {
$tr = this._renderRow(fileData, options);
this.$fileList.append($tr);
}
}
this.isEmpty = false;
this.files.splice(index, 0, fileData);
if ($tr && options.animate) {
$tr.addClass('appear transparent');
window.setTimeout(function() {
$tr.removeClass('transparent');
$("#fileList tr").removeClass('mouseOver');
$tr.addClass('mouseOver');
});
}
if (options.scrollTo) {
this.scrollTo(fileData.name);
}
// defaults to true if not defined
if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
this.fileSummary.add(fileData, true);
this.updateEmptyContent();
}
return $tr;
},
/**
* Creates a new row element based on the given attributes
* and returns it.
*
* @param {OC.Files.FileInfo} fileData map of file attributes
* @param {Object} [options] map of attributes
* @param {int} [options.index] index at which to insert the element
* @param {boolean} [options.updateSummary] true to update the summary
* after adding (default), false otherwise. Defaults to true.
* @param {boolean} [options.animate] true to animate the thumbnail image after load
* defaults to true.
* @return new tr element (not appended to the table)
*/
_renderRow: function(fileData, options) {
options = options || {};
var type = fileData.type || 'file',
mime = fileData.mimetype,
path = fileData.path || this.getCurrentDirectory(),
permissions = parseInt(fileData.permissions, 10) || 0;
var isEndToEndEncrypted = (type === 'dir' && fileData.isEncrypted);
if (!isEndToEndEncrypted && fileData.isShareMountPoint) {
permissions = permissions | OC.PERMISSION_UPDATE;
}
if (type === 'dir') {
mime = mime || 'httpd/unix-directory';
}
var tr = this._createRow(
fileData,
options
);
var filenameTd = tr.find('td.filename');
// TODO: move dragging to FileActions ?
// enable drag only for deletable files
if (this._dragOptions && permissions & OC.PERMISSION_DELETE) {
filenameTd.draggable(this._dragOptions);
}
// allow dropping on folders
if (this._folderDropOptions && mime === 'httpd/unix-directory') {
tr.droppable(this._folderDropOptions);
}
if (options.hidden) {
tr.addClass('hidden');
}
if (this._isHiddenFile(fileData)) {
tr.addClass('hidden-file');
}
// display actions
this.fileActions.display(filenameTd, !options.silent, this);
if (mime !== 'httpd/unix-directory' && fileData.hasPreview !== false) {
var iconDiv = filenameTd.find('.thumbnail');
// lazy load / newly inserted td ?
// the typeof check ensures that the default value of animate is true
if (typeof(options.animate) === 'undefined' || !!options.animate) {
this.lazyLoadPreview({
fileId: fileData.id,
path: path + '/' + fileData.name,
mime: mime,
etag: fileData.etag,
callback: function(url) {
iconDiv.css('background-image', 'url("' + url + '")');
}
});
}
else {
// set the preview URL directly
var urlSpec = {
file: path + '/' + fileData.name,
c: fileData.etag
};
var previewUrl = this.generatePreviewUrl(urlSpec);
previewUrl = previewUrl.replace(/\(/g, '%28').replace(/\)/g, '%29');
iconDiv.css('background-image', 'url("' + previewUrl + '")');
}
}
return tr;
},
/**
* Returns the current directory
* @method getCurrentDirectory
* @return current directory
*/
getCurrentDirectory: function(){
return this._currentDirectory || this.$el.find('#dir').val() || '/';
},
/**
* Returns the directory permissions
* @return permission value as integer
*/
getDirectoryPermissions: function() {
return this && this.dirInfo && this.dirInfo.permissions ? this.dirInfo.permissions : parseInt(this.$el.find('#permissions').val(), 10);
},
/**
* Changes the current directory and reload the file list.
* @param {string} targetDir target directory (non URL encoded)
* @param {boolean} [changeUrl=true] if the URL must not be changed (defaults to true)
* @param {boolean} [force=false] set to true to force changing directory
* @param {string} [fileId] optional file id, if known, to be appended in the URL
*/
changeDirectory: function(targetDir, changeUrl, force, fileId) {
var self = this;
var currentDir = this.getCurrentDirectory();
targetDir = targetDir || '/';
if (!force && currentDir === targetDir) {
return;
}
this._setCurrentDir(targetDir, changeUrl, fileId);
// discard finished uploads list, we'll get it through a regular reload
this._uploads = {};
return this.reload().then(function(success){
if (!success) {
self.changeDirectory(currentDir, true);
}
});
},
linkTo: function(dir) {
return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
},
/**
* @param {string} path
* @returns {boolean}
*/
_isValidPath: function(path) {
var sections = path.split('/');
for (var i = 0; i < sections.length; i++) {
if (sections[i] === '..') {
return false;
}
}
return path.toLowerCase().indexOf(decodeURI('%0a')) === -1 &&
path.toLowerCase().indexOf(decodeURI('%00')) === -1;
},
/**
* Sets the current directory name and updates the breadcrumb.
* @param targetDir directory to display
* @param changeUrl true to also update the URL, false otherwise (default)
* @param {string} [fileId] file id
*/
_setCurrentDir: function(targetDir, changeUrl, fileId) {
targetDir = targetDir.replace(/\\/g, '/');
if (!this._isValidPath(targetDir)) {
targetDir = '/';
changeUrl = true;
}
var previousDir = this.getCurrentDirectory(),
baseDir = OC.basename(targetDir);
if (baseDir !== '') {
this.setPageTitle(baseDir);
}
else {
this.setPageTitle();
}
if (targetDir.length > 0 && targetDir[0] !== '/') {
targetDir = '/' + targetDir;
}
this._currentDirectory = targetDir;
// legacy stuff
this.$el.find('#dir').val(targetDir);
if (changeUrl !== false) {
var params = {
dir: targetDir,
previousDir: previousDir
};
if (fileId) {
params.fileId = fileId;
}
this.$el.trigger(jQuery.Event('changeDirectory', params));
}
this.breadcrumb.setDirectory(this.getCurrentDirectory());
},
/**
* Sets the current sorting and refreshes the list
*
* @param sort sort attribute name
* @param direction sort direction, one of "asc" or "desc"
* @param update true to update the list, false otherwise (default)
* @param persist true to save changes in the database (default)
*/
setSort: function(sort, direction, update, persist) {
var comparator = FileList.Comparators[sort] || FileList.Comparators.name;
this._sort = sort;
this._sortDirection = (direction === 'desc')?'desc':'asc';
this._sortComparator = function(fileInfo1, fileInfo2) {
var isFavorite = function(fileInfo) {
return fileInfo.tags && fileInfo.tags.indexOf(OC.TAG_FAVORITE) >= 0;
};
if (isFavorite(fileInfo1) && !isFavorite(fileInfo2)) {
return -1;
} else if (!isFavorite(fileInfo1) && isFavorite(fileInfo2)) {
return 1;
}
return direction === 'asc' ? comparator(fileInfo1, fileInfo2) : -comparator(fileInfo1, fileInfo2);
};
this.$el.find('thead th .sort-indicator')
.removeClass(this.SORT_INDICATOR_ASC_CLASS)
.removeClass(this.SORT_INDICATOR_DESC_CLASS)
.toggleClass('hidden', true)
.addClass(this.SORT_INDICATOR_DESC_CLASS);
this.$el.find('thead th.column-' + sort + ' .sort-indicator')
.removeClass(this.SORT_INDICATOR_ASC_CLASS)
.removeClass(this.SORT_INDICATOR_DESC_CLASS)
.toggleClass('hidden', false)
.addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS);
if (update) {
if (this._clientSideSort) {
this.files.sort(this._sortComparator);
this.setFiles(this.files);
}
else {
this.reload();
}
}
if (persist && OC.getCurrentUser().uid) {
$.post(OC.generateUrl('/apps/files/api/v1/sorting'), {
mode: sort,
direction: direction
});
}
},
/**
* Returns list of webdav properties to request
*/
_getWebdavProperties: function() {
return [].concat(this.filesClient.getPropfindProperties());
},
/**
* Reloads the file list using ajax call
*
* @return ajax call object
*/
reload: function() {
this._selectedFiles = {};
this._selectionSummary.clear();
if (this._currentFileModel) {
this._currentFileModel.off();
}
this._currentFileModel = null;
this.$el.find('.select-all').prop('checked', false);
this.showMask();
this._reloadCall = this.filesClient.getFolderContents(
this.getCurrentDirectory(), {
includeParent: true,
properties: this._getWebdavProperties()
}
);
if (this._detailsView) {
// close sidebar
this._updateDetailsView(null);
}
this._setCurrentDir(this.getCurrentDirectory(), false);
var callBack = this.reloadCallback.bind(this);
return this._reloadCall.then(callBack, callBack);
},
reloadCallback: function(status, result) {
delete this._reloadCall;
this.hideMask();
if (status === 401) {
return false;
}
// Firewall Blocked request?
if (status === 403) {
// Go home
this.changeDirectory('/');
OC.Notification.show(t('files', 'This operation is forbidden'), {type: 'error'});
return false;
}
// Did share service die or something else fail?
if (status === 500) {
// Go home
this.changeDirectory('/');
OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'),
{type: 'error'}
);
return false;
}
if (status === 503) {
// Go home
if (this.getCurrentDirectory() !== '/') {
this.changeDirectory('/');
// TODO: read error message from exception
OC.Notification.show(t('files', 'Storage is temporarily not available'),
{type: 'error'}
);
}
return false;
}
if (status === 400 || status === 404 || status === 405) {
// go back home
this.changeDirectory('/');
return false;
}
// aborted ?
if (status === 0){
return true;
}
this.updateStorageStatistics(true);
// first entry is the root
this.dirInfo = result.shift();
this.breadcrumb.setDirectoryInfo(this.dirInfo);
if (this.dirInfo.permissions) {
this._updateDirectoryPermissions();
}
result.sort(this._sortComparator);
this.setFiles(result);
if (this.dirInfo) {
var newFileId = this.dirInfo.id;
// update fileid in URL
var params = {
dir: this.getCurrentDirectory()
};
if (newFileId) {
params.fileId = newFileId;
}
this.$el.trigger(jQuery.Event('afterChangeDirectory', params));
}
return true;
},
updateStorageStatistics: function(force) {
OCA.Files.Files.updateStorageStatistics(this.getCurrentDirectory(), force);
},
updateStorageQuotas: function() {
OCA.Files.Files.updateStorageQuotas();
},
/**
* @deprecated do not use nor override
*/
getAjaxUrl: function(action, params) {
return OCA.Files.Files.getAjaxUrl(action, params);
},
getDownloadUrl: function(files, dir, isDir) {
return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir);
},
getUploadUrl: function(fileName, dir) {
if (_.isUndefined(dir)) {
dir = this.getCurrentDirectory();
}
var pathSections = dir.split('/');
if (!_.isUndefined(fileName)) {
pathSections.push(fileName);
}
var encodedPath = '';
_.each(pathSections, function(section) {
if (section !== '') {
encodedPath += '/' + encodeURIComponent(section);
}
});
return OC.linkToRemoteBase('webdav') + encodedPath;
},
/**
* Generates a preview URL based on the URL space.
* @param urlSpec attributes for the URL
* @param {int} urlSpec.x width
* @param {int} urlSpec.y height
* @param {String} urlSpec.file path to the file
* @return preview URL
*/
generatePreviewUrl: function(urlSpec) {
urlSpec = urlSpec || {};
if (!urlSpec.x) {
urlSpec.x = this.$table.data('preview-x') || 250;
}
if (!urlSpec.y) {
urlSpec.y = this.$table.data('preview-y') || 250;
}
urlSpec.x *= window.devicePixelRatio;
urlSpec.y *= window.devicePixelRatio;
urlSpec.x = Math.ceil(urlSpec.x);
urlSpec.y = Math.ceil(urlSpec.y);
urlSpec.forceIcon = 0;
if (typeof urlSpec.fileId !== 'undefined') {
delete urlSpec.file;
return OC.generateUrl('/core/preview?') + $.param(urlSpec);
} else {
delete urlSpec.fileId;
return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
}
},
/**
* Lazy load a file's preview.
*
* @param path path of the file
* @param mime mime type
* @param callback callback function to call when the image was loaded
* @param etag file etag (for caching)
*/
lazyLoadPreview : function(options) {
var self = this;
var fileId = options.fileId;
var path = options.path;
var mime = options.mime;
var ready = options.callback;
var etag = options.etag;
// get mime icon url
var iconURL = OC.MimeType.getIconUrl(mime);
var previewURL,
urlSpec = {};
ready(iconURL); // set mimeicon URL
urlSpec.fileId = fileId;
urlSpec.file = OCA.Files.Files.fixPath(path);
if (options.x) {
urlSpec.x = options.x;
}
if (options.y) {
urlSpec.y = options.y;
}
if (options.a) {
urlSpec.a = options.a;
}
if (options.mode) {
urlSpec.mode = options.mode;
}
if (etag){
// use etag as cache buster
urlSpec.c = etag;
}
previewURL = self.generatePreviewUrl(urlSpec);
previewURL = previewURL.replace(/\(/g, '%28').replace(/\)/g, '%29');
// preload image to prevent delay
// this will make the browser cache the image
var img = new Image();
img.onload = function(){
// if loading the preview image failed (no preview for the mimetype) then img.width will < 5
if (img.width > 5) {
ready(previewURL, img);
} else if (options.error) {
options.error();
}
};
if (options.error) {
img.onerror = options.error;
}
img.src = previewURL;
},
_updateDirectoryPermissions: function() {
var isCreatable = (this.dirInfo.permissions & OC.PERMISSION_CREATE) !== 0 && this.$el.find('#free_space').val() !== '0';
this.$el.find('#permissions').val(this.dirInfo.permissions);
this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
},
/**
* Shows/hides action buttons
*
* @param show true for enabling, false for disabling
*/
showActions: function(show){
this.$el.find('.actions,#file_action_panel').toggleClass('hidden', !show);
if (show){
// make sure to display according to permissions
var permissions = this.getDirectoryPermissions();
var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
// remove old style breadcrumbs (some apps might create them)
this.$el.find('#controls .crumb').remove();
// refresh breadcrumbs in case it was replaced by an app
this.breadcrumb.render();
}
else{
this.$el.find('.creatable, .notCreatable').addClass('hidden');
}
},
/**
* Enables/disables viewer mode.
* In viewer mode, apps can embed themselves under the controls bar.
* In viewer mode, the actions of the file list will be hidden.
* @param show true for enabling, false for disabling
*/
setViewerMode: function(show){
this.showActions(!show);
this.$el.find('#filestable').toggleClass('hidden', show);
this.$el.trigger(new $.Event('changeViewerMode', {viewerModeEnabled: show}));
},
/**
* Removes a file entry from the list
* @param name name of the file to remove
* @param {Object} [options] map of attributes
* @param {boolean} [options.updateSummary] true to update the summary
* after removing, false otherwise. Defaults to true.
* @return deleted element
*/
remove: function(name, options){
options = options || {};
var fileEl = this.findFileEl(name);
var fileData = _.findWhere(this.files, {name: name});
if (!fileData) {
return;
}
var fileId = fileData.id;
if (this._selectedFiles[fileId]) {
// remove from selection first
this._selectFileEl(fileEl, false);
this.updateSelectionSummary();
}
if (this._selectedFiles[fileId]) {
delete this._selectedFiles[fileId];
this._selectionSummary.remove(fileData);
this.updateSelectionSummary();
}
var index = this.files.findIndex(function(el){return el.name==name;});
this.files.splice(index, 1);
// TODO: improve performance on batch update
this.isEmpty = !this.files.length;
if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
this.updateEmptyContent();
this.fileSummary.remove({type: fileData.type, size: fileData.size}, true);
}
if (!fileEl.length) {
return null;
}
if (this._dragOptions && (fileEl.data('permissions') & OC.PERMISSION_DELETE)) {
// file is only draggable when delete permissions are set
fileEl.find('td.filename').draggable('destroy');
}
if (this._currentFileModel && this._currentFileModel.get('id') === fileId) {
// Note: in the future we should call destroy() directly on the model
// and the model will take care of the deletion.
// Here we only trigger the event to notify listeners that
// the file was removed.
this._currentFileModel.trigger('destroy');
this._updateDetailsView(null);
}
fileEl.remove();
var lastIndex = this.$fileList.children().length;
// if there are less elements visible than one page
// but there are still pending elements in the array,
// then directly append the next page
if (lastIndex < this.files.length && lastIndex < this.pageSize()) {
this._nextPage(true);
}
return fileEl;
},
/**
* Finds the index of the row before which the given
* fileData should be inserted, considering the current
* sorting
*
* @param {OC.Files.FileInfo} fileData file info
*/
_findInsertionIndex: function(fileData) {
var index = 0;
while (index < this.files.length && this._sortComparator(fileData, this.files[index]) > 0) {
index++;
}
return index;
},
/**
* Moves a file to a given target folder.
*
* @param fileNames array of file names to move
* @param targetPath absolute target path
* @param callback function to call when movement is finished
* @param dir the dir path where fileNames are located (optionnal, will take current folder if undefined)
*/
move: function(fileNames, targetPath, callback, dir) {
var self = this;
dir = typeof dir === 'string' ? dir : this.getCurrentDirectory();
if (dir.charAt(dir.length - 1) !== '/') {
dir += '/';
}
var target = OC.basename(targetPath);
if (!_.isArray(fileNames)) {
fileNames = [fileNames];
}
var moveFileFunction = function(fileName) {
var $tr = self.findFileEl(fileName);
self.showFileBusyState($tr, true);
if (targetPath.charAt(targetPath.length - 1) !== '/') {
// make sure we move the files into the target dir,
// not overwrite it
targetPath = targetPath + '/';
}
return self.filesClient.move(dir + fileName, targetPath + fileName)
.done(function() {
// if still viewing the same directory
if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
// recalculate folder size
var oldFile = self.findFileEl(target);
var newFile = self.findFileEl(fileName);
var oldSize = oldFile.data('size');
var newSize = oldSize + newFile.data('size');
oldFile.data('size', newSize);
oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
self.remove(fileName);
}
})
.fail(function(status) {
if (status === 412) {
// TODO: some day here we should invoke the conflict dialog
OC.Notification.show(t('files', 'Could not move "{file}", target exists',
{file: fileName}), {type: 'error'}
);
} else {
OC.Notification.show(t('files', 'Could not move "{file}"',
{file: fileName}), {type: 'error'}
);
}
})
.always(function() {
self.showFileBusyState($tr, false);
});
};
return this.reportOperationProgress(fileNames, moveFileFunction, callback);
},
_reflect: function (promise){
return promise.then(function(v){ return {};}, function(e){ return {};});
},
reportOperationProgress: function (fileNames, operationFunction, callback){
var self = this;
self._operationProgressBar.showProgressBar(false);
var mcSemaphore = new OCA.Files.Semaphore(5);
var counter = 0;
var promises = _.map(fileNames, function(arg) {
return mcSemaphore.acquire().then(function(){
return operationFunction(arg).always(function(){
mcSemaphore.release();
counter++;
self._operationProgressBar.setProgressBarValue(100.0*counter/fileNames.length);
});
});
});
return Promise.all(_.map(promises, self._reflect)).then(function(){
if (callback) {
callback();
}
self._operationProgressBar.hideProgressBar();
});
},
/**
* Copies a file to a given target folder.
*
* @param fileNames array of file names to copy
* @param targetPath absolute target path
* @param callback to call when copy is finished with success
* @param dir the dir path where fileNames are located (optionnal, will take current folder if undefined)
*/
copy: function(fileNames, targetPath, callback, dir) {
var self = this;
var filesToNotify = [];
var count = 0;
dir = typeof dir === 'string' ? dir : this.getCurrentDirectory();
if (dir.charAt(dir.length - 1) !== '/') {
dir += '/';
}
var target = OC.basename(targetPath);
if (!_.isArray(fileNames)) {
fileNames = [fileNames];
}
var copyFileFunction = function(fileName) {
var $tr = self.findFileEl(fileName);
self.showFileBusyState($tr, true);
if (targetPath.charAt(targetPath.length - 1) !== '/') {
// make sure we move the files into the target dir,
// not overwrite it
targetPath = targetPath + '/';
}
var targetPathAndName = targetPath + fileName;
if ((dir + fileName) === targetPathAndName) {
var dotIndex = targetPathAndName.indexOf(".");
if ( dotIndex > 1) {
var leftPartOfName = targetPathAndName.substr(0, dotIndex);
var fileNumber = leftPartOfName.match(/\d+/);
// TRANSLATORS name that is appended to copied files with the same name, will be put in parenthesis and appened with a number if it is the second+ copy
var copyNameLocalized = t('files', 'copy');
if (isNaN(fileNumber) ) {
fileNumber++;
targetPathAndName = targetPathAndName.replace(/(?=\.[^.]+$)/g, " (" + copyNameLocalized + " " + fileNumber + ")");
}
else {
// Check if we have other files with 'copy X' and the same name
var maxNum = 1;
if (self.files !== null) {
leftPartOfName = leftPartOfName.replace("/", "");
leftPartOfName = leftPartOfName.replace(new RegExp("\\(" + copyNameLocalized + "( \\d+)?\\)"),"");
// find the last file with the number extension and add one to the new name
for (var j = 0; j < self.files.length; j++) {
var cName = self.files[j].name;
if (cName.indexOf(leftPartOfName) > -1) {
if (cName.indexOf("(" + copyNameLocalized + ")") > 0) {
targetPathAndName = targetPathAndName.replace(new RegExp(" \\(" + copyNameLocalized + "\\)"),"");
if (maxNum == 1) {
maxNum = 2;
}
}
else {
var cFileNumber = cName.match(new RegExp("\\(" + copyNameLocalized + " (\\d+)\\)"));
if (cFileNumber && parseInt(cFileNumber[1]) >= maxNum) {
maxNum = parseInt(cFileNumber[1]) + 1;
}
}
}
}
targetPathAndName = targetPathAndName.replace(new RegExp(" \\(" + copyNameLocalized + " \\d+\\)"),"");
}
// Create the new file name with _x at the end
// Start from 2 per a special request of the 'standard'
var extensionName = " (" + copyNameLocalized + " " + maxNum +")";
if (maxNum == 1) {
extensionName = " (" + copyNameLocalized + ")";
}
targetPathAndName = targetPathAndName.replace(/(?=\.[^.]+$)/g, extensionName);
}
}
}
return self.filesClient.copy(dir + fileName, targetPathAndName)
.done(function () {
filesToNotify.push(fileName);
// if still viewing the same directory
if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
// recalculate folder size
var oldFile = self.findFileEl(target);
var newFile = self.findFileEl(fileName);
var oldSize = oldFile.data('size');
var newSize = oldSize + newFile.data('size');
oldFile.data('size', newSize);
oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
}
self.reload();
})
.fail(function(status) {
if (status === 412) {
// TODO: some day here we should invoke the conflict dialog
OC.Notification.show(t('files', 'Could not copy "{file}", target exists',
{file: fileName}), {type: 'error'}
);
} else {
OC.Notification.show(t('files', 'Could not copy "{file}"',
{file: fileName}), {type: 'error'}
);
}
})
.always(function() {
self.showFileBusyState($tr, false);
count++;
/**
* We only show the notifications once the last file has been copied
*/
if (count === fileNames.length) {
// Remove leading and ending /
if (targetPath.slice(0, 1) === '/') {
targetPath = targetPath.slice(1, targetPath.length);
}
if (targetPath.slice(-1) === '/') {
targetPath = targetPath.slice(0, -1);
}
if (filesToNotify.length > 0) {
// Since there's no visual indication that the files were copied, let's send some notifications !
if (filesToNotify.length === 1) {
OC.Notification.show(t('files', 'Copied {origin} inside {destination}',
{
origin: filesToNotify[0],
destination: targetPath
}
), {timeout: 10});
} else if (filesToNotify.length > 0 && filesToNotify.length < 3) {
OC.Notification.show(t('files', 'Copied {origin} inside {destination}',
{
origin: filesToNotify.join(', '),
destination: targetPath
}
), {timeout: 10});
} else {
OC.Notification.show(t('files', 'Copied {origin} and {nbfiles} other files inside {destination}',
{
origin: filesToNotify[0],
nbfiles: filesToNotify.length - 1,
destination: targetPath
}
), {timeout: 10});
}
}
}
});
};
return this.reportOperationProgress(fileNames, copyFileFunction, callback);
},
/**
* Updates the given row with the given file info
*
* @param {Object} $tr row element
* @param {OCA.Files.FileInfo} fileInfo file info
* @param {Object} options options
*
* @return {Object} new row element
*/
updateRow: function($tr, fileInfo, options) {
$tr.find('[data-original-title]').tooltip('hide');
this.files.splice($tr.index(), 1);
$tr.remove();
options = _.extend({silent: true}, options);
options = _.extend(options, {updateSummary: false});
$tr = this.add(fileInfo, options);
this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $tr}));
return $tr;
},
/**
* Triggers file rename input field for the given file name.
* If the user enters a new name, the file will be renamed.
*
* @param oldName file name of the file to rename
*/
rename: function(oldName) {
var self = this;
var tr, td, input, form;
tr = this.findFileEl(oldName);
var oldFileInfo = this.files[tr.index()];
tr.data('renaming',true);
td = tr.children('td.filename');
input = $('<input type="text" class="filename"/>').val(oldName);
form = $('<form></form>');
form.append(input);
td.children('a.name').children(':not(.thumbnail-wrapper)').hide();
td.append(form);
input.focus();
//preselect input
var len = input.val().lastIndexOf('.');
if ( len === -1 ||
tr.data('type') === 'dir' ) {
len = input.val().length;
}
input.selectRange(0, len);
var checkInput = function () {
var filename = input.val();
if (filename !== oldName) {
// Files.isFileNameValid(filename) throws an exception itself
OCA.Files.Files.isFileNameValid(filename);
if (self.inList(filename)) {
throw t('files', '{newName} already exists', {newName: filename}, undefined, {
escape: false
});
}
}
return true;
};
function restore() {
input.tooltip('hide');
tr.data('renaming',false);
form.remove();
td.children('a.name').children(':not(.thumbnail-wrapper)').show();
}
function updateInList(fileInfo) {
self.updateRow(tr, fileInfo);
self._updateDetailsView(fileInfo.name, false);
}
// TODO: too many nested blocks, move parts into functions
form.submit(function(event) {
event.stopPropagation();
event.preventDefault();
if (input.hasClass('error')) {
return;
}
try {
var newName = input.val().trim();
input.tooltip('hide');
form.remove();
if (newName !== oldName) {
checkInput();
// mark as loading (temp element)
self.showFileBusyState(tr, true);
tr.attr('data-file', newName);
var basename = newName;
if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
basename = newName.substr(0, newName.lastIndexOf('.'));
}
td.find('a.name span.nametext').text(basename);
td.children('a.name').children(':not(.thumbnail-wrapper)').show();
var path = tr.attr('data-path') || self.getCurrentDirectory();
self.filesClient.move(OC.joinPaths(path, oldName), OC.joinPaths(path, newName))
.done(function() {
oldFileInfo.name = newName;
updateInList(oldFileInfo);
})
.fail(function(status) {
// TODO: 409 means current folder does not exist, redirect ?
if (status === 404) {
// source not found, so remove it from the list
OC.Notification.show(t('files', 'Could not rename "{fileName}", it does not exist any more',
{fileName: oldName}), {timeout: 7, type: 'error'}
);
self.remove(newName, {updateSummary: true});
return;
} else if (status === 412) {
// target exists
OC.Notification.show(
t('files', 'The name "{targetName}" is already used in the folder "{dir}". Please choose a different name.',
{
targetName: newName,
dir: self.getCurrentDirectory(),
}),
{
type: 'error'
}
);
} else {
// restore the item to its previous state
OC.Notification.show(t('files', 'Could not rename "{fileName}"',
{fileName: oldName}), {type: 'error'}
);
}
updateInList(oldFileInfo);
});
} else {
// add back the old file info when cancelled
self.files.splice(tr.index(), 1);
tr.remove();
tr = self.add(oldFileInfo, {updateSummary: false, silent: true});
self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
}
} catch (error) {
input.attr('title', error);
input.tooltip({placement: 'right', trigger: 'manual'});
input.tooltip('fixTitle');
input.tooltip('show');
input.addClass('error');
}
return false;
});
input.keyup(function(event) {
// verify filename on typing
try {
checkInput();
input.tooltip('hide');
input.removeClass('error');
} catch (error) {
input.attr('title', error);
input.tooltip({placement: 'right', trigger: 'manual'});
input.tooltip('fixTitle');
input.tooltip('show');
input.addClass('error');
}
if (event.keyCode === 27) {
restore();
}
});
input.click(function(event) {
event.stopPropagation();
event.preventDefault();
});
input.blur(function() {
if(input.hasClass('error')) {
restore();
} else {
form.trigger('submit');
}
});
},
/**
* Create an empty file inside the current directory.
*
* @param {string} name name of the file
*
* @return {Promise} promise that will be resolved after the
* file was created
*
* @since 8.2
*/
createFile: function(name) {
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
OCA.Files.Files.isFileNameValid(name);
if (this.lastAction) {
this.lastAction();
}
name = this.getUniqueName(name);
var targetPath = this.getCurrentDirectory() + '/' + name;
self.filesClient.putFileContents(
targetPath,
' ', // dont create empty files which fails on some storage backends
{
contentType: 'text/plain',
overwrite: true
}
)
.done(function() {
// TODO: error handling / conflicts
self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) {
deferred.resolve(status, data);
}, function() {
OC.Notification.show(t('files', 'Could not create file "{file}"',
{file: name}), {type: 'error'}
);
});
})
.fail(function(status) {
if (status === 412) {
OC.Notification.show(t('files', 'Could not create file "{file}" because it already exists',
{file: name}), {type: 'error'}
);
} else {
OC.Notification.show(t('files', 'Could not create file "{file}"',
{file: name}), {type: 'error'}
);
}
deferred.reject(status);
});
return promise;
},
/**
* Create a directory inside the current directory.
*
* @param {string} name name of the directory
*
* @return {Promise} promise that will be resolved after the
* directory was created
*
* @since 8.2
*/
createDirectory: function(name) {
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
OCA.Files.Files.isFileNameValid(name);
if (this.lastAction) {
this.lastAction();
}
name = this.getUniqueName(name);
var targetPath = this.getCurrentDirectory() + '/' + name;
this.filesClient.createDirectory(targetPath)
.done(function() {
self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) {
deferred.resolve(status, data);
}, function() {
OC.Notification.show(t('files', 'Could not create folder "{dir}"',
{dir: name}), {type: 'error'}
);
});
})
.fail(function(createStatus) {
// method not allowed, folder might exist already
if (createStatus === 405) {
// add it to the list, for completeness
self.addAndFetchFileInfo(targetPath, '', {scrollTo:true})
.done(function(status, data) {
OC.Notification.show(t('files', 'Could not create folder "{dir}" because it already exists',
{dir: name}), {type: 'error'}
);
// still consider a failure
deferred.reject(createStatus, data);
})
.fail(function() {
OC.Notification.show(t('files', 'Could not create folder "{dir}"',
{dir: name}), {type: 'error'}
);
deferred.reject(status);
});
} else {
OC.Notification.show(t('files', 'Could not create folder "{dir}"',
{dir: name}), {type: 'error'}
);
deferred.reject(createStatus);
}
});
return promise;
},
/**
* Add file into the list by fetching its information from the server first.
*
* If the given directory does not match the current directory, nothing will
* be fetched.
*
* @param {String} fileName file name
* @param {String} [dir] optional directory, defaults to the current one
* @param {Object} options same options as #add
* @return {Promise} promise that resolves with the file info, or an
* already resolved Promise if no info was fetched. The promise rejects
* if the file was not found or an error occurred.
*
* @since 9.0
*/
addAndFetchFileInfo: function(fileName, dir, options) {
var self = this;
var deferred = $.Deferred();
if (_.isUndefined(dir)) {
dir = this.getCurrentDirectory();
} else {
dir = dir || '/';
}
var targetPath = OC.joinPaths(dir, fileName);
if ((OC.dirname(targetPath) || '/') !== this.getCurrentDirectory()) {
// no need to fetch information
deferred.resolve();
return deferred.promise();
}
var addOptions = _.extend({
animate: true,
scrollTo: false
}, options || {});
this.filesClient.getFileInfo(targetPath, {
properties: this._getWebdavProperties()
})
.then(function(status, data) {
// remove first to avoid duplicates
self.remove(data.name);
self.add(data, addOptions);
deferred.resolve(status, data);
})
.fail(function(status) {
OC.Notification.show(t('files', 'Could not create file "{file}"',
{file: name}), {type: 'error'}
);
deferred.reject(status);
});
return deferred.promise();
},
/**
* Returns whether the given file name exists in the list
*
* @param {string} file file name
*
* @return {bool} true if the file exists in the list, false otherwise
*/
inList:function(file) {
return this.findFile(file);
},
/**
* Shows busy state on a given file row or multiple
*
* @param {string|Array.<string>} files file name or array of file names
* @param {bool} [busy=true] busy state, true for busy, false to remove busy state
*
* @since 8.2
*/
showFileBusyState: function(files, state) {
var self = this;
if (!_.isArray(files) && !files.is) {
files = [files];
}
if (_.isUndefined(state)) {
state = true;
}
_.each(files, function(fileName) {
// jquery element already ?
var $tr;
if (_.isString(fileName)) {
$tr = self.findFileEl(fileName);
} else {
$tr = $(fileName);
}
var $thumbEl = $tr.find('.thumbnail');
$tr.toggleClass('busy', state);
if (state) {
$thumbEl.parent().addClass('icon-loading-small');
} else {
$thumbEl.parent().removeClass('icon-loading-small');
}
});
},
/**
* Delete the given files from the given dir
* @param files file names list (without path)
* @param dir directory in which to delete the files, defaults to the current
* directory
*/
do_delete:function(files, dir) {
var self = this;
if (files && files.substr) {
files=[files];
}
if (!files) {
// delete all files in directory
files = _.pluck(this.files, 'name');
}
// Finish any existing actions
if (this.lastAction) {
this.lastAction();
}
dir = dir || this.getCurrentDirectory();
var removeFunction = function(fileName) {
var $tr = self.findFileEl(fileName);
self.showFileBusyState($tr, true);
return self.filesClient.remove(dir + '/' + fileName)
.done(function() {
if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
self.remove(fileName);
}
})
.fail(function(status) {
if (status === 404) {
// the file already did not exist, remove it from the list
if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
self.remove(fileName);
}
} else {
// only reset the spinner for that one file
OC.Notification.show(t('files', 'Error deleting file "{fileName}".',
{fileName: fileName}), {type: 'error'}
);
}
})
.always(function() {
self.showFileBusyState($tr, false);
});
};
return this.reportOperationProgress(files, removeFunction).then(function(){
self.updateStorageStatistics();
self.updateStorageQuotas();
});
},
/**
* Creates the file summary section
*/
_createSummary: function() {
var $tr = $('<tr class="summary"></tr>');
if (this._allowSelection) {
// Dummy column for selection, as all rows must have the same
// number of columns.
$tr.append('<td></td>');
}
this.$el.find('tfoot').append($tr);
return new OCA.Files.FileSummary($tr, {config: this._filesConfig});
},
updateEmptyContent: function() {
var permissions = this.getDirectoryPermissions();
var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('#emptycontent .uploadmessage').toggleClass('hidden', !isCreatable || !this.isEmpty);
this.$el.find('#filestable').toggleClass('hidden', this.isEmpty);
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
},
/**
* Shows the loading mask.
*
* @see OCA.Files.FileList#hideMask
*/
showMask: function() {
// in case one was shown before
var $mask = this.$el.find('.mask');
if ($mask.exists()) {
return;
}
this.$table.addClass('hidden');
this.$el.find('#emptycontent').addClass('hidden');
$mask = $('<div class="mask transparent icon-loading"></div>');
this.$el.append($mask);
$mask.removeClass('transparent');
},
/**
* Hide the loading mask.
* @see OCA.Files.FileList#showMask
*/
hideMask: function() {
this.$el.find('.mask').remove();
this.$table.removeClass('hidden');
},
scrollTo:function(file) {
if (!_.isArray(file)) {
file = [file];
}
if (file.length === 1) {
_.defer(function() {
this.showDetailsView(file[0]);
}.bind(this));
}
this.highlightFiles(file, function($tr) {
$tr.addClass('searchresult');
$tr.one('hover', function() {
$tr.removeClass('searchresult');
});
});
},
/**
* @deprecated use setFilter(filter)
*/
filter:function(query) {
this.setFilter('');
},
/**
* @deprecated use setFilter('')
*/
unfilter:function() {
this.setFilter('');
},
/**
* hide files matching the given filter
* @param filter
*/
setFilter:function(filter) {
var total = 0;
if (this._filter === filter) {
return;
}
this._filter = filter;
this.fileSummary.setFilter(filter, this.files);
total = this.fileSummary.getTotal();
if (!this.$el.find('.mask').exists()) {
this.hideIrrelevantUIWhenNoFilesMatch();
}
var visibleCount = 0;
filter = filter.toLowerCase();
function filterRows(tr) {
var $e = $(tr);
if ($e.data('file').toString().toLowerCase().indexOf(filter) === -1) {
$e.addClass('hidden');
} else {
visibleCount++;
$e.removeClass('hidden');
}
}
var $trs = this.$fileList.find('tr');
do {
_.each($trs, filterRows);
if (visibleCount < total) {
$trs = this._nextPage(false);
}
} while (visibleCount < total && $trs.length > 0);
this.$container.trigger('scroll');
},
hideIrrelevantUIWhenNoFilesMatch:function() {
if (this._filter && this.fileSummary.summary.totalDirs + this.fileSummary.summary.totalFiles === 0) {
this.$el.find('#filestable thead th').addClass('hidden');
this.$el.find('#emptycontent').addClass('hidden');
$('#searchresults').addClass('filter-empty');
$('#searchresults .emptycontent').addClass('emptycontent-search');
if ( $('#searchresults').length === 0 || $('#searchresults').hasClass('hidden') ) {
var error = t('files', 'No search results in other folders for {tag}{filter}{endtag}', {filter:this._filter});
this.$el.find('.nofilterresults').removeClass('hidden').
find('p').html(error.replace('{tag}', '<strong>').replace('{endtag}', '</strong>'));
}
} else {
$('#searchresults').removeClass('filter-empty');
$('#searchresults .emptycontent').removeClass('emptycontent-search');
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
if (!this.$el.find('.mask').exists()) {
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
}
this.$el.find('.nofilterresults').addClass('hidden');
}
},
/**
* get the current filter
* @param filter
*/
getFilter:function(filter) {
return this._filter;
},
/**
* update the search object to use this filelist when filtering
*/
updateSearch:function() {
if (OCA.Search.files) {
OCA.Search.files.setFileList(this);
}
if (OC.Search) {
OC.Search.clear();
}
},
/**
* Update UI based on the current selection
*/
updateSelectionSummary: function() {
var summary = this._selectionSummary.summary;
var selection;
var showHidden = !!this._filesConfig.get('showhidden');
if (summary.totalFiles === 0 && summary.totalDirs === 0) {
this.$el.find('#headerName a.name>span:first').text(t('files','Name'));
this.$el.find('#headerSize a>span:first').text(t('files','Size'));
this.$el.find('#modified a>span:first').text(t('files','Modified'));
this.$el.find('table').removeClass('multiselect');
this.$el.find('.selectedActions').addClass('hidden');
}
else {
this.$el.find('.selectedActions').removeClass('hidden');
this.$el.find('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize));
var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
if (summary.totalDirs > 0 && summary.totalFiles > 0) {
var selectionVars = {
dirs: directoryInfo,
files: fileInfo
};
selection = t('files', '{dirs} and {files}', selectionVars);
} else if (summary.totalDirs > 0) {
selection = directoryInfo;
} else {
selection = fileInfo;
}
if (!showHidden && summary.totalHidden > 0) {
var hiddenInfo = n('files', 'including %n hidden', 'including %n hidden', summary.totalHidden);
selection += ' (' + hiddenInfo + ')';
}
this.$el.find('#headerName a.name>span:first').text(selection);
this.$el.find('#modified a>span:first').text('');
this.$el.find('table').addClass('multiselect');
if (this.fileMultiSelectMenu) {
this.fileMultiSelectMenu.toggleItemVisibility('download', this.isSelectedDownloadable());
this.fileMultiSelectMenu.toggleItemVisibility('delete', this.isSelectedDeletable());
this.fileMultiSelectMenu.toggleItemVisibility('copyMove', this.isSelectedCopiable());
if (this.isSelectedCopiable()) {
if (this.isSelectedMovable()) {
this.fileMultiSelectMenu.updateItemText('copyMove', t('files', 'Move or copy'));
} else {
this.fileMultiSelectMenu.updateItemText('copyMove', t('files', 'Copy'));
}
} else {
this.fileMultiSelectMenu.toggleItemVisibility('copyMove', false);
}
}
}
},
/**
* Check whether all selected files are copiable
*/
isSelectedCopiable: function() {
return _.reduce(this.getSelectedFiles(), function(copiable, file) {
var requiredPermission = $('#isPublic').val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ;
return copiable && (file.permissions & requiredPermission);
}, true);
},
/**
* Check whether all selected files are movable
*/
isSelectedMovable: function() {
return _.reduce(this.getSelectedFiles(), function(movable, file) {
return movable && (file.permissions & OC.PERMISSION_UPDATE);
}, true);
},
/**
* Check whether all selected files are downloadable
*/
isSelectedDownloadable: function() {
return _.reduce(this.getSelectedFiles(), function(downloadable, file) {
return downloadable && (file.permissions & OC.PERMISSION_READ);
}, true);
},
/**
* Check whether all selected files are deletable
*/
isSelectedDeletable: function() {
return _.reduce(this.getSelectedFiles(), function(deletable, file) {
return deletable && (file.permissions & OC.PERMISSION_DELETE);
}, true);
},
/**
* Are all files selected?
*
* @returns {Boolean} all files are selected
*/
isAllSelected: function() {
var checkbox = this.$el.find('.select-all')
var checked = checkbox.prop('checked')
var indeterminate = checkbox.prop('indeterminate')
return checked && !indeterminate;
},
/**
* Returns the file info of the selected files
*
* @return array of file names
*/
getSelectedFiles: function() {
return _.values(this._selectedFiles);
},
getUniqueName: function(name) {
if (this.findFileEl(name).exists()) {
var numMatch;
var parts=name.split('.');
var extension = "";
if (parts.length > 1) {
extension=parts.pop();
}
var base=parts.join('.');
numMatch=base.match(/\((\d+)\)/);
var num=2;
if (numMatch && numMatch.length>0) {
num=parseInt(numMatch[numMatch.length-1], 10)+1;
base=base.split('(');
base.pop();
base=$.trim(base.join('('));
}
name=base+' ('+num+')';
if (extension) {
name = name+'.'+extension;
}
// FIXME: ugly recursion
return this.getUniqueName(name);
}
return name;
},
/**
* Shows a "permission denied" notification
*/
_showPermissionDeniedNotification: function() {
var message = t('files', 'You don’t have permission to upload or create files here');
OC.Notification.show(message, {type: 'error'});
},
/**
* Setup file upload events related to the file-upload plugin
*
* @param {OC.Uploader} uploader
*/
setupUploadEvents: function(uploader) {
var self = this;
self._uploads = {};
// detect the progress bar resize
uploader.on('resized', this._onResize);
uploader.on('drop', function(e, data) {
self._uploader.log('filelist handle fileuploaddrop', e, data);
if (self.$el.hasClass('hidden')) {
// do not upload to invisible lists
e.preventDefault();
return false;
}
var dropTarget = $(e.delegatedEvent.target);
// check if dropped inside this container and not another one
if (dropTarget.length
&& !self.$el.is(dropTarget) // dropped on list directly
&& !self.$el.has(dropTarget).length // dropped inside list
&& !dropTarget.is(self.$container) // dropped on main container
&& !self.$el.parent().is(dropTarget) // drop on the parent container (#app-content) since the main container might not have the full height
) {
e.preventDefault();
return false;
}
// find the closest tr or crumb to use as target
dropTarget = dropTarget.closest('tr, .crumb');
// if dropping on tr or crumb, drag&drop upload to folder
if (dropTarget && (dropTarget.data('type') === 'dir' ||
dropTarget.hasClass('crumb'))) {
// remember as context
data.context = dropTarget;
// if permissions are specified, only allow if create permission is there
var permissions = dropTarget.data('permissions');
if (!_.isUndefined(permissions) && (permissions & OC.PERMISSION_CREATE) === 0) {
self._showPermissionDeniedNotification();
return false;
}
var dir = dropTarget.data('file');
// if from file list, need to prepend parent dir
if (dir) {
var parentDir = self.getCurrentDirectory();
if (parentDir[parentDir.length - 1] !== '/') {
parentDir += '/';
}
dir = parentDir + dir;
}
else{
// read full path from crumb
dir = dropTarget.data('dir') || '/';
}
// add target dir
data.targetDir = dir;
} else {
// cancel uploads to current dir if no permission
var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0;
if (!isCreatable) {
self._showPermissionDeniedNotification();
e.stopPropagation();
return false;
}
// we are dropping somewhere inside the file list, which will
// upload the file to the current directory
data.targetDir = self.getCurrentDirectory();
}
});
uploader.on('add', function(e, data) {
self._uploader.log('filelist handle fileuploadadd', e, data);
// add ui visualization to existing folder
if (data.context && data.context.data('type') === 'dir') {
// add to existing folder
// update upload counter ui
var uploadText = data.context.find('.uploadtext');
var currentUploads = parseInt(uploadText.attr('currentUploads'), 10);
currentUploads += 1;
uploadText.attr('currentUploads', currentUploads);
var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads);
if (currentUploads === 1) {
self.showFileBusyState(uploadText.closest('tr'), true);
uploadText.text(translatedText);
uploadText.show();
} else {
uploadText.text(translatedText);
}
}
if (!data.targetDir) {
data.targetDir = self.getCurrentDirectory();
}
});
/*
* when file upload done successfully add row to filelist
* update counter when uploading to sub folder
*/
uploader.on('done', function(e, upload) {
var data = upload.data;
self._uploader.log('filelist handle fileuploaddone', e, data);
var status = data.jqXHR.status;
if (status < 200 || status >= 300) {
// error was handled in OC.Uploads already
return;
}
var fileName = upload.getFileName();
var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath());
if (!self._uploads) {
self._uploads = {};
}
if (OC.isSamePath(OC.dirname(upload.getFullPath() + '/'), self.getCurrentDirectory())) {
self._uploads[fileName] = fetchInfoPromise;
}
var uploadText = self.$fileList.find('tr .uploadtext');
self.showFileBusyState(uploadText.closest('tr'), false);
uploadText.fadeOut();
uploadText.attr('currentUploads', 0);
self.updateStorageQuotas();
});
uploader.on('createdfolder', function(fullPath) {
self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath));
});
uploader.on('stop', function() {
self._uploader.log('filelist handle fileuploadstop');
// prepare list of uploaded file names in the current directory
// and discard the other ones
var promises = _.values(self._uploads);
var fileNames = _.keys(self._uploads);
self._uploads = [];
// as soon as all info is fetched
$.when.apply($, promises).then(function() {
// highlight uploaded files
self.highlightFiles(fileNames);
self.updateStorageStatistics();
});
var uploadText = self.$fileList.find('tr .uploadtext');
self.showFileBusyState(uploadText.closest('tr'), false);
uploadText.fadeOut();
uploadText.attr('currentUploads', 0);
});
uploader.on('fail', function(e, data) {
self._uploader.log('filelist handle fileuploadfail', e, data);
self._uploads = [];
//if user pressed cancel hide upload chrome
//cleanup uploading to a dir
var uploadText = self.$fileList.find('tr .uploadtext');
self.showFileBusyState(uploadText.closest('tr'), false);
uploadText.fadeOut();
uploadText.attr('currentUploads', 0);
self.updateStorageStatistics();
});
},
/**
* Scroll to the last file of the given list
* Highlight the list of files
* @param files array of filenames,
* @param {Function} [highlightFunction] optional function
* to be called after the scrolling is finished
*/
highlightFiles: function(files, highlightFunction) {
// Detection of the uploaded element
var filename = files[files.length - 1];
var $fileRow = this.findFileEl(filename);
while(!$fileRow.exists() && this._nextPage(false) !== false) { // Checking element existence
$fileRow = this.findFileEl(filename);
}
if (!$fileRow.exists()) { // Element not present in the file list
return;
}
var currentOffset = this.$container.scrollTop();
var additionalOffset = this.$el.find("#controls").height()+this.$el.find("#controls").offset().top;
// Animation
var _this = this;
var $scrollContainer = this.$container;
if ($scrollContainer[0] === window) {
// need to use "html" to animate scrolling
// when the scroll container is the window
$scrollContainer = $('html');
}
$scrollContainer.animate({
// Scrolling to the top of the new element
scrollTop: currentOffset + $fileRow.offset().top - $fileRow.height() * 2 - additionalOffset
}, {
duration: 500,
complete: function() {
// Highlighting function
var highlightRow = highlightFunction;
if (!highlightRow) {
highlightRow = function($fileRow) {
$fileRow.addClass("highlightUploaded");
setTimeout(function() {
$fileRow.removeClass("highlightUploaded");
}, 2500);
};
}
// Loop over uploaded files
for(var i=0; i<files.length; i++) {
var $fileRow = _this.findFileEl(files[i]);
if($fileRow.length !== 0) { // Checking element existence
highlightRow($fileRow);
}
}
}
});
},
_renderNewButton: function() {
// if an upload button (legacy) already exists or no actions container exist, skip
var $actionsContainer = this.$el.find('#controls .actions');
if (!$actionsContainer.length || this.$el.find('.button.upload').length) {
return;
}
var $newButton = $(OCA.Files.Templates['template_addbutton']({
addText: t('files', 'New'),
iconClass: 'icon-add'
}));
$actionsContainer.prepend($newButton);
$newButton.tooltip({'placement': 'bottom'});
$newButton.click(_.bind(this._onClickNewButton, this));
this._newButton = $newButton;
},
_onClickNewButton: function(event) {
var $target = $(event.target);
if (!$target.hasClass('.button')) {
$target = $target.closest('.button');
}
this._newButton.tooltip('hide');
event.preventDefault();
if ($target.hasClass('disabled')) {
return false;
}
if (!this._newFileMenu) {
this._newFileMenu = new OCA.Files.NewFileMenu({
fileList: this
});
$('.actions').append(this._newFileMenu.$el);
}
this._newFileMenu.showAt($target);
return false;
},
/**
* Register a tab view to be added to all views
*/
registerTabView: function(tabView) {
if (this._detailsView) {
this._detailsView.addTabView(tabView);
}
},
/**
* Register a detail view to be added to all views
*/
registerDetailView: function(detailView) {
if (this._detailsView) {
this._detailsView.addDetailView(detailView);
}
},
/**
* Register a view to be added to the breadcrumb view
*/
registerBreadCrumbDetailView: function(detailView) {
if (this.breadcrumb) {
this.breadcrumb.addDetailView(detailView);
}
},
/**
* Returns the registered detail views.
*
* @return null|Array<OCA.Files.DetailFileInfoView> an array with the
* registered DetailFileInfoViews, or null if the details view
* is not enabled.
*/
getRegisteredDetailViews: function() {
if (this._detailsView) {
return this._detailsView.getDetailViews();
}
return null;
}
};
FileList.MultiSelectMenuActions = {
ToggleSelectionModeAction: function(fileList) {
return {
name: 'toggleSelectionMode',
displayName: function(context) {
return t('files', 'Select file range');
},
iconClass: 'icon-fullscreen',
action: function() {
fileList._onClickToggleSelectionMode();
},
};
},
},
/**
* Sort comparators.
* @namespace OCA.Files.FileList.Comparators
* @private
*/
FileList.Comparators = {
/**
* Compares two file infos by name, making directories appear
* first.
*
* @param {OC.Files.FileInfo} fileInfo1 file info
* @param {OC.Files.FileInfo} fileInfo2 file info
* @return {int} -1 if the first file must appear before the second one,
* 0 if they are identify, 1 otherwise.
*/
name: function(fileInfo1, fileInfo2) {
if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') {
return -1;
}
if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') {
return 1;
}
return OC.Util.naturalSortCompare(fileInfo1.name, fileInfo2.name);
},
/**
* Compares two file infos by size.
*
* @param {OC.Files.FileInfo} fileInfo1 file info
* @param {OC.Files.FileInfo} fileInfo2 file info
* @return {int} -1 if the first file must appear before the second one,
* 0 if they are identify, 1 otherwise.
*/
size: function(fileInfo1, fileInfo2) {
return fileInfo1.size - fileInfo2.size;
},
/**
* Compares two file infos by timestamp.
*
* @param {OC.Files.FileInfo} fileInfo1 file info
* @param {OC.Files.FileInfo} fileInfo2 file info
* @return {int} -1 if the first file must appear before the second one,
* 0 if they are identify, 1 otherwise.
*/
mtime: function(fileInfo1, fileInfo2) {
return fileInfo1.mtime - fileInfo2.mtime;
}
};
/**
* File info attributes.
*
* @typedef {Object} OC.Files.FileInfo
*
* @lends OC.Files.FileInfo
*
* @deprecated use OC.Files.FileInfo instead
*
*/
OCA.Files.FileInfo = OC.Files.FileInfo;
OCA.Files.FileList = FileList;
})();
$(document).ready(function() {
// FIXME: unused ?
OCA.Files.FileList.useUndo = (window.onbeforeunload)?true:false;
$(window).on('beforeunload', function () {
if (OCA.Files.FileList.lastAction) {
OCA.Files.FileList.lastAction();
}
});
$(window).on('unload', function () {
$(window).trigger('beforeunload');
});
});
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* Construct a new FileActions instance
* @constructs Files
*/
var Files = function() {
this.initialize();
};
/**
* @memberof OCA.Search
*/
Files.prototype = {
fileList: null,
/**
* Initialize the file search
*/
initialize: function() {
var self = this;
this.fileAppLoaded = function() {
return !!OCA.Files && !!OCA.Files.App;
};
function inFileList($row, result) {
if (! self.fileAppLoaded()) {
return false;
}
var dir = self.fileList.getCurrentDirectory().replace(/\/+$/,'');
var resultDir = OC.dirname(result.path);
return dir === resultDir && self.fileList.inList(result.name);
}
function updateLegacyMimetype(result) {
// backward compatibility:
if (!result.mime && result.mime_type) {
result.mime = result.mime_type;
}
}
function hideNoFilterResults() {
var $nofilterresults = $('.nofilterresults');
if ( ! $nofilterresults.hasClass('hidden') ) {
$nofilterresults.addClass('hidden');
}
}
this.renderFolderResult = function($row, result) {
if (inFileList($row, result)) {
return null;
}
hideNoFilterResults();
/*render folder icon, show path beneath filename,
show size and last modified date on the right */
this.updateLegacyMimetype(result);
var $pathDiv = $('<div class="path"></div>').text(result.path.substr(1, result.path.lastIndexOf("/")));
$row.find('td.info div.name').after($pathDiv).text(result.name);
$row.find('td.result a').attr('href', result.link);
$row.find('td.icon').css('background-image', 'url(' + OC.MimeType.getIconUrl(result.mime) + ')');
return $row;
};
this.renderFileResult = function($row, result) {
if (inFileList($row, result)) {
return null;
}
hideNoFilterResults();
/*render preview icon, show path beneath filename,
show size and last modified date on the right */
this.updateLegacyMimetype(result);
var $pathDiv = $('<div class="path"></div>').text(result.path.substr(1, result.path.lastIndexOf("/")));
$row.find('td.info div.name').after($pathDiv).text(result.name);
$row.find('td.result a').attr('href', result.link);
if (self.fileAppLoaded()) {
self.fileList.lazyLoadPreview({
path: result.path,
mime: result.mime,
callback: function (url) {
$row.find('td.icon').css('background-image', 'url(' + url + ')');
}
});
} else {
// FIXME how to get mime icon if not in files app
var mimeicon = result.mime.replace('/', '-');
$row.find('td.icon').css('background-image', 'url(' + OC.MimeType.getIconUrl(result.mime) + ')');
var dir = OC.dirname(result.path);
if (dir === '') {
dir = '/';
}
$row.find('td.info a').attr('href',
OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', {dir: dir, scrollto: result.name})
);
}
return $row;
};
this.handleFolderClick = function($row, result, event) {
// open folder
if (self.fileAppLoaded() && self.fileList.id === 'files') {
self.fileList.changeDirectory(result.path);
return false;
} else {
return true;
}
};
this.handleFileClick = function($row, result, event) {
if (self.fileAppLoaded() && self.fileList.id === 'files') {
self.fileList.changeDirectory(OC.dirname(result.path));
self.fileList.scrollTo(result.name);
return false;
} else {
return true;
}
};
this.updateLegacyMimetype = function (result) {
// backward compatibility:
if (!result.mime && result.mime_type) {
result.mime = result.mime_type;
}
};
this.setFileList = function (fileList) {
this.fileList = fileList;
};
OC.Plugins.register('OCA.Search.Core', this);
},
attach: function(search) {
var self = this;
search.setFilter('files', function (query) {
if (self.fileAppLoaded()) {
self.fileList.setFilter(query);
if (query.length > 2) {
//search is not started until 500msec have passed
window.setTimeout(function() {
$('.nofilterresults').addClass('hidden');
}, 500);
}
}
});
search.setRenderer('folder', this.renderFolderResult.bind(this));
search.setRenderer('file', this.renderFileResult.bind(this));
search.setRenderer('image', this.renderFileResult.bind(this));
search.setRenderer('audio', this.renderFileResult.bind(this));
search.setHandler('folder', this.handleFolderClick.bind(this));
search.setHandler(['file', 'audio', 'image'], this.handleFileClick.bind(this));
if (self.fileAppLoaded()) {
// hide results when switching directory outside of search results
$('#app-content').delegate('>div', 'changeDirectory', function() {
search.clear();
});
}
}
};
OCA.Search.Files = Files;
OCA.Search.files = new Files();
})();
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
// HACK: this piece needs to be loaded AFTER the files app (for unit tests)
$(document).ready(function() {
(function(OCA) {
/**
* @class OCA.Files.FavoritesFileList
* @augments OCA.Files.FavoritesFileList
*
* @classdesc Favorites file list.
* Displays the list of files marked as favorites
*
* @param $el container element with existing markup for the #controls
* and a table
* @param [options] map of options, see other parameters
*/
var FavoritesFileList = function($el, options) {
this.initialize($el, options);
};
FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Files.FavoritesFileList.prototype */ {
id: 'favorites',
appName: t('files','Favorites'),
_clientSideSort: true,
_allowSelection: false,
/**
* @private
*/
initialize: function($el, options) {
OCA.Files.FileList.prototype.initialize.apply(this, arguments);
if (this.initialized) {
return;
}
OC.Plugins.attach('OCA.Files.FavoritesFileList', this);
},
updateEmptyContent: function() {
var dir = this.getCurrentDirectory();
if (dir === '/') {
// root has special permissions
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
}
else {
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
}
},
getDirectoryPermissions: function() {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
},
updateStorageStatistics: function() {
// no op because it doesn't have
// storage info like free space / used space
},
reload: function() {
this.showMask();
if (this._reloadCall) {
this._reloadCall.abort();
}
// there is only root
this._setCurrentDir('/', false);
this._reloadCall = this.filesClient.getFilteredFiles(
{
favorite: true
},
{
properties: this._getWebdavProperties()
}
);
var callBack = this.reloadCallback.bind(this);
return this._reloadCall.then(callBack, callBack);
},
reloadCallback: function(status, result) {
if (result) {
// prepend empty dir info because original handler
result.unshift({});
}
return OCA.Files.FileList.prototype.reloadCallback.call(this, status, result);
},
});
OCA.Files.FavoritesFileList = FavoritesFileList;
})(OCA);
});
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
// HACK: this piece needs to be loaded AFTER the files app (for unit tests)
$(document).ready(function () {
(function (OCA) {
/**
* @class OCA.Files.RecentFileList
* @augments OCA.Files.RecentFileList
*
* @classdesc Recent file list.
* Displays the list of recently modified files
*
* @param $el container element with existing markup for the #controls
* and a table
* @param [options] map of options, see other parameters
*/
var RecentFileList = function ($el, options) {
options.sorting = {
mode: 'mtime',
direction: 'desc'
};
this.initialize($el, options);
this._allowSorting = false;
};
RecentFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Files.RecentFileList.prototype */ {
id: 'recent',
appName: t('files', 'Recent'),
_clientSideSort: true,
_allowSelection: false,
/**
* @private
*/
initialize: function () {
OCA.Files.FileList.prototype.initialize.apply(this, arguments);
if (this.initialized) {
return;
}
OC.Plugins.attach('OCA.Files.RecentFileList', this);
},
updateEmptyContent: function () {
var dir = this.getCurrentDirectory();
if (dir === '/') {
// root has special permissions
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
}
else {
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
}
},
getDirectoryPermissions: function () {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
},
updateStorageStatistics: function () {
// no op because it doesn't have
// storage info like free space / used space
},
reload: function () {
this.showMask();
if (this._reloadCall) {
this._reloadCall.abort();
}
// there is only root
this._setCurrentDir('/', false);
this._reloadCall = $.ajax({
url: OC.generateUrl('/apps/files/api/v1/recent'),
type: 'GET',
dataType: 'json'
});
var callBack = this.reloadCallback.bind(this);
return this._reloadCall.then(callBack, callBack);
},
reloadCallback: function (result) {
delete this._reloadCall;
this.hideMask();
if (result.files) {
this.setFiles(result.files.sort(this._sortComparator));
return true;
}
return false;
}
});
OCA.Files.RecentFileList = RecentFileList;
})(OCA);
});
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/* global Handlebars */
(function (OCA) {
_.extend(OC.Files.Client, {
PROPERTY_TAGS: '{' + OC.Files.Client.NS_OWNCLOUD + '}tags',
PROPERTY_FAVORITE: '{' + OC.Files.Client.NS_OWNCLOUD + '}favorite'
});
/**
* Returns the icon class for the matching state
*
* @param {boolean} state true if starred, false otherwise
* @return {string} icon class for star image
*/
function getStarIconClass (state) {
return state ? 'icon-starred' : 'icon-star';
}
/**
* Render the star icon with the given state
*
* @param {boolean} state true if starred, false otherwise
* @return {Object} jQuery object
*/
function renderStar (state) {
return OCA.Files.Templates['favorite_mark']({
isFavorite: state,
altText: state ? t('files', 'Favorited') : t('files', 'Not favorited'),
iconClass: getStarIconClass(state)
});
}
/**
* Toggle star icon on favorite mark element
*
* @param {Object} $favoriteMarkEl favorite mark element
* @param {boolean} state true if starred, false otherwise
*/
function toggleStar ($favoriteMarkEl, state) {
$favoriteMarkEl.removeClass('icon-star icon-starred').addClass(getStarIconClass(state));
$favoriteMarkEl.toggleClass('permanent', state);
}
/**
* Remove Item from Quickaccesslist
*
* @param {String} appfolder folder to be removed
*/
function removeFavoriteFromList (appfolder) {
var quickAccessList = 'sublist-favorites';
var listULElements = document.getElementById(quickAccessList);
if (!listULElements) {
return;
}
var apppath=appfolder;
if(appfolder.startsWith("//")){
apppath=appfolder.substring(1, appfolder.length);
}
$(listULElements).find('[data-dir="' + _.escape(apppath) + '"]').remove();
if (listULElements.childElementCount === 0) {
var collapsibleButton = $(listULElements).parent().find('button.collapse');
collapsibleButton.hide();
$("#button-collapse-parent-favorites").removeClass('collapsible');
}
}
/**
* Add Item to Quickaccesslist
*
* @param {String} appfolder folder to be added
*/
function addFavoriteToList (appfolder) {
var quickAccessList = 'sublist-favorites';
var listULElements = document.getElementById(quickAccessList);
if (!listULElements) {
return;
}
var listLIElements = listULElements.getElementsByTagName('li');
var appName = appfolder.substring(appfolder.lastIndexOf("/") + 1, appfolder.length);
var apppath = appfolder;
if(appfolder.startsWith("//")){
apppath = appfolder.substring(1, appfolder.length);
}
var url = OC.generateUrl('/apps/files/?dir=' + apppath + '&view=files');
var innerTagA = document.createElement('A');
innerTagA.setAttribute("href", url);
innerTagA.setAttribute("class", "nav-icon-files svg");
innerTagA.innerHTML = _.escape(appName);
var length = listLIElements.length + 1;
var innerTagLI = document.createElement('li');
innerTagLI.setAttribute("data-id", apppath.replace('/', '-'));
innerTagLI.setAttribute("data-dir", apppath);
innerTagLI.setAttribute("data-view", 'files');
innerTagLI.setAttribute("class", "nav-" + appName);
innerTagLI.setAttribute("folderpos", length.toString());
innerTagLI.appendChild(innerTagA);
$.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/NodeType"),{folderpath: apppath}, function (data, status) {
if (data === "dir") {
if (listULElements.childElementCount <= 0) {
listULElements.appendChild(innerTagLI);
var collapsibleButton = $(listULElements).parent().find('button.collapse');
collapsibleButton.show();
$(listULElements).parent().addClass('collapsible');
} else {
listLIElements[listLIElements.length - 1].after(innerTagLI);
}
}
}
);
}
OCA.Files = OCA.Files || {};
/**
* Extends the file actions and file list to include a favorite mark icon
* and a favorite action in the file actions menu; it also adds "data-tags"
* and "data-favorite" attributes to file elements.
*
* @namespace OCA.Files.TagsPlugin
*/
OCA.Files.TagsPlugin = {
name: 'Tags',
allowedLists: [
'files',
'favorites',
'systemtags',
'shares.self',
'shares.others',
'shares.link'
],
_extendFileActions: function (fileActions) {
var self = this;
fileActions.registerAction({
name: 'Favorite',
displayName: function (context) {
var $file = context.$file;
var isFavorite = $file.data('favorite') === true;
if (isFavorite) {
return t('files', 'Remove from favorites');
}
// As it is currently not possible to provide a context for
// the i18n strings "Add to favorites" was used instead of
// "Favorite" to remove the ambiguity between verb and noun
// when it is translated.
return t('files', 'Add to favorites');
},
mime: 'all',
order: -100,
permissions: OC.PERMISSION_NONE,
iconClass: function (fileName, context) {
var $file = context.$file;
var isFavorite = $file.data('favorite') === true;
if (isFavorite) {
return 'icon-star-dark';
}
return 'icon-starred';
},
actionHandler: function (fileName, context) {
var $favoriteMarkEl = context.$file.find('.favorite-mark');
var $file = context.$file;
var fileInfo = context.fileList.files[$file.index()];
var dir = context.dir || context.fileList.getCurrentDirectory();
var tags = $file.attr('data-tags');
if (_.isUndefined(tags)) {
tags = '';
}
tags = tags.split('|');
tags = _.without(tags, '');
var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0;
if (isFavorite) {
// remove tag from list
tags = _.without(tags, OC.TAG_FAVORITE);
removeFavoriteFromList(dir + '/' + fileName);
} else {
tags.push(OC.TAG_FAVORITE);
addFavoriteToList(dir + '/' + fileName);
}
// pre-toggle the star
toggleStar($favoriteMarkEl, !isFavorite);
context.fileInfoModel.trigger('busy', context.fileInfoModel, true);
self.applyFileTags(
dir + '/' + fileName,
tags,
$favoriteMarkEl,
isFavorite
).then(function (result) {
context.fileInfoModel.trigger('busy', context.fileInfoModel, false);
// response from server should contain updated tags
var newTags = result.tags;
if (_.isUndefined(newTags)) {
newTags = tags;
}
context.fileInfoModel.set({
'tags': newTags,
'favorite': !isFavorite
});
});
}
});
},
_extendFileList: function (fileList) {
// extend row prototype
var oldCreateRow = fileList._createRow;
fileList._createRow = function (fileData) {
var $tr = oldCreateRow.apply(this, arguments);
var isFavorite = false;
if (fileData.tags) {
$tr.attr('data-tags', fileData.tags.join('|'));
if (fileData.tags.indexOf(OC.TAG_FAVORITE) >= 0) {
$tr.attr('data-favorite', true);
isFavorite = true;
}
}
var $icon = $(renderStar(isFavorite));
$tr.find('td.filename .thumbnail').append($icon);
return $tr;
};
var oldElementToFile = fileList.elementToFile;
fileList.elementToFile = function ($el) {
var fileInfo = oldElementToFile.apply(this, arguments);
var tags = $el.attr('data-tags');
if (_.isUndefined(tags)) {
tags = '';
}
tags = tags.split('|');
tags = _.without(tags, '');
fileInfo.tags = tags;
return fileInfo;
};
var oldGetWebdavProperties = fileList._getWebdavProperties;
fileList._getWebdavProperties = function () {
var props = oldGetWebdavProperties.apply(this, arguments);
props.push(OC.Files.Client.PROPERTY_TAGS);
props.push(OC.Files.Client.PROPERTY_FAVORITE);
return props;
};
fileList.filesClient.addFileInfoParser(function (response) {
var data = {};
var props = response.propStat[0].properties;
var tags = props[OC.Files.Client.PROPERTY_TAGS];
var favorite = props[OC.Files.Client.PROPERTY_FAVORITE];
if (tags && tags.length) {
tags = _.chain(tags).filter(function (xmlvalue) {
return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'tag');
}).map(function (xmlvalue) {
return xmlvalue.textContent || xmlvalue.text;
}).value();
}
if (tags) {
data.tags = tags;
}
if (favorite && parseInt(favorite, 10) !== 0) {
data.tags = data.tags || [];
data.tags.push(OC.TAG_FAVORITE);
}
return data;
});
},
attach: function (fileList) {
if (this.allowedLists.indexOf(fileList.id) < 0) {
return;
}
this._extendFileActions(fileList.fileActions);
this._extendFileList(fileList);
},
/**
* Replaces the given files' tags with the specified ones.
*
* @param {String} fileName path to the file or folder to tag
* @param {Array.<String>} tagNames array of tag names
* @param {Object} $favoriteMarkEl favorite mark element
* @param {boolean} isFavorite Was the item favorited before
*/
applyFileTags: function (fileName, tagNames, $favoriteMarkEl, isFavorite) {
var encodedPath = OC.encodePath(fileName);
while (encodedPath[0] === '/') {
encodedPath = encodedPath.substr(1);
}
return $.ajax({
url: OC.generateUrl('/apps/files/api/v1/files/') + encodedPath,
contentType: 'application/json',
data: JSON.stringify({
tags: tagNames || []
}),
dataType: 'json',
type: 'POST'
}).fail(function (response) {
var message = '';
// show message if it is available
if (response.responseJSON && response.responseJSON.message) {
message = ': ' + response.responseJSON.message;
}
OC.Notification.show(t('files', 'An error occurred while trying to update the tags' + message), {type: 'error'});
toggleStar($favoriteMarkEl, isFavorite);
});
}
};
})
(OCA);
OC.Plugins.register('OCA.Files.FileList', OCA.Files.TagsPlugin);
/*
* Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function (OCA) {
OCA.Files = OCA.Files || {};
/**
* @namespace OCA.Files.GotoPlugin
*
*/
OCA.Files.GotoPlugin = {
name: 'Goto',
disallowedLists: [
'files',
'trashbin'
],
attach: function (fileList) {
if (this.disallowedLists.indexOf(fileList.id) !== -1) {
return;
}
var fileActions = fileList.fileActions;
fileActions.registerAction({
name: 'Goto',
displayName: t('files', 'View in folder'),
mime: 'all',
permissions: OC.PERMISSION_ALL,
iconClass: 'icon-goto nav-icon-extstoragemounts',
type: OCA.Files.FileActions.TYPE_DROPDOWN,
actionHandler: function (fileName, context) {
var fileModel = context.fileInfoModel;
OC.Apps.hideAppSidebar($('.detailsView'));
OCA.Files.App.setActiveView('files', {silent: true});
OCA.Files.App.fileList.changeDirectory(fileModel.get('path'), true, true).then(function() {
OCA.Files.App.fileList.scrollTo(fileModel.get('name'));
});
},
render: function (actionSpec, isDefault, context) {
return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context)
.removeClass('permanent');
}
});
}
};
})(OCA);
OC.Plugins.register('OCA.Files.FileList', OCA.Files.GotoPlugin);
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function(OCA) {
/**
* Registers the favorites file list from the files app sidebar.
*
* @namespace OCA.Files.FavoritesPlugin
*/
OCA.Files.FavoritesPlugin = {
name: 'Favorites',
/**
* @type OCA.Files.FavoritesFileList
*/
favoritesFileList: null,
attach: function() {
var self = this;
$('#app-content-favorites').on('show.plugin-favorites', function(e) {
self.showFileList($(e.target));
});
$('#app-content-favorites').on('hide.plugin-favorites', function() {
self.hideFileList();
});
},
detach: function() {
if (this.favoritesFileList) {
this.favoritesFileList.destroy();
OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated);
OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated);
$('#app-content-favorites').off('.plugin-favorites');
this.favoritesFileList = null;
}
},
showFileList: function($el) {
if (!this.favoritesFileList) {
this.favoritesFileList = this._createFavoritesFileList($el);
}
return this.favoritesFileList;
},
hideFileList: function() {
if (this.favoritesFileList) {
this.favoritesFileList.$fileList.empty();
}
},
/**
* Creates the favorites file list.
*
* @param $el container for the file list
* @return {OCA.Files.FavoritesFileList} file list
*/
_createFavoritesFileList: function($el) {
var fileActions = this._createFileActions();
// register favorite list for sidebar section
return new OCA.Files.FavoritesFileList(
$el, {
fileActions: fileActions,
// The file list is created when a "show" event is handled,
// so it should be marked as "shown" like it would have been
// done if handling the event with the file list already
// created.
shown: true
}
);
},
_createFileActions: function() {
// inherit file actions from the files app
var fileActions = new OCA.Files.FileActions();
// note: not merging the legacy actions because legacy apps are not
// compatible with the sharing overview and need to be adapted first
fileActions.registerDefaultActions();
fileActions.merge(OCA.Files.fileActions);
if (!this._globalActionsInitialized) {
// in case actions are registered later
this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
OCA.Files.fileActions.on('setDefault.plugin-favorites', this._onActionsUpdated);
OCA.Files.fileActions.on('registerAction.plugin-favorites', this._onActionsUpdated);
this._globalActionsInitialized = true;
}
// when the user clicks on a folder, redirect to the corresponding
// folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
OCA.Files.App.setActiveView('files', {silent: true});
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true);
});
fileActions.setDefault('dir', 'Open');
return fileActions;
},
_onActionsUpdated: function(ev) {
if (ev.action) {
this.favoritesFileList.fileActions.registerAction(ev.action);
} else if (ev.defaultAction) {
this.favoritesFileList.fileActions.setDefault(
ev.defaultAction.mime,
ev.defaultAction.name
);
}
}
};
})(OCA);
OC.Plugins.register('OCA.Files.App', OCA.Files.FavoritesPlugin);
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function (OCA) {
/**
* Registers the recent file list from the files app sidebar.
*
* @namespace OCA.Files.RecentPlugin
*/
OCA.Files.RecentPlugin = {
name: 'Recent',
/**
* @type OCA.Files.RecentFileList
*/
recentFileList: null,
attach: function () {
var self = this;
$('#app-content-recent').on('show.plugin-recent', function (e) {
self.showFileList($(e.target));
});
$('#app-content-recent').on('hide.plugin-recent', function () {
self.hideFileList();
});
},
detach: function () {
if (this.recentFileList) {
this.recentFileList.destroy();
OCA.Files.fileActions.off('setDefault.plugin-recent', this._onActionsUpdated);
OCA.Files.fileActions.off('registerAction.plugin-recent', this._onActionsUpdated);
$('#app-content-recent').off('.plugin-recent');
this.recentFileList = null;
}
},
showFileList: function ($el) {
if (!this.recentFileList) {
this.recentFileList = this._createRecentFileList($el);
}
return this.recentFileList;
},
hideFileList: function () {
if (this.recentFileList) {
this.recentFileList.$fileList.empty();
}
},
/**
* Creates the recent file list.
*
* @param $el container for the file list
* @return {OCA.Files.RecentFileList} file list
*/
_createRecentFileList: function ($el) {
var fileActions = this._createFileActions();
// register recent list for sidebar section
return new OCA.Files.RecentFileList(
$el, {
fileActions: fileActions,
// The file list is created when a "show" event is handled,
// so it should be marked as "shown" like it would have been
// done if handling the event with the file list already
// created.
shown: true
}
);
},
_createFileActions: function () {
// inherit file actions from the files app
var fileActions = new OCA.Files.FileActions();
// note: not merging the legacy actions because legacy apps are not
// compatible with the sharing overview and need to be adapted first
fileActions.registerDefaultActions();
fileActions.merge(OCA.Files.fileActions);
if (!this._globalActionsInitialized) {
// in case actions are registered later
this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
OCA.Files.fileActions.on('setDefault.plugin-recent', this._onActionsUpdated);
OCA.Files.fileActions.on('registerAction.plugin-recent', this._onActionsUpdated);
this._globalActionsInitialized = true;
}
// when the user clicks on a folder, redirect to the corresponding
// folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
OCA.Files.App.setActiveView('files', {silent: true});
var path = OC.joinPaths(context.$file.attr('data-path'), filename);
OCA.Files.App.fileList.changeDirectory(path, true, true);
});
fileActions.setDefault('dir', 'Open');
return fileActions;
},
_onActionsUpdated: function (ev) {
if (ev.action) {
this.recentFileList.fileActions.registerAction(ev.action);
} else if (ev.defaultAction) {
this.recentFileList.fileActions.setDefault(
ev.defaultAction.mime,
ev.defaultAction.name
);
}
}
};
})(OCA);
OC.Plugins.register('OCA.Files.App', OCA.Files.RecentPlugin);
/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* @class OCA.Files.DetailFileInfoView
* @classdesc
*
* Displays a block of details about the file info.
*
*/
var DetailFileInfoView = OC.Backbone.View.extend({
tagName: 'div',
className: 'detailFileInfoView',
_template: null,
/**
* returns the jQuery object for HTML output
*
* @returns {jQuery}
*/
get$: function() {
return this.$el;
},
/**
* Sets the file info to be displayed in the view
*
* @param {OCA.Files.FileInfo} fileInfo file info to set
*/
setFileInfo: function(fileInfo) {
this.model = fileInfo;
this.render();
},
/**
* Returns the file info.
*
* @return {OCA.Files.FileInfo} file info
*/
getFileInfo: function() {
return this.model;
}
});
OCA.Files.DetailFileInfoView = DetailFileInfoView;
})();
/*
* Copyright (c) 2016
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function () {
var SidebarPreviewManager = function (fileList) {
this._fileList = fileList;
this._previewHandlers = {};
OC.Plugins.attach('OCA.Files.SidebarPreviewManager', this);
};
SidebarPreviewManager.prototype = {
addPreviewHandler: function (mime, handler) {
this._previewHandlers[mime] = handler;
},
getMimeTypePreviewHandler: function(mime) {
var mimePart = mime.split('/').shift();
if (this._previewHandlers[mime]) {
return this._previewHandlers[mime];
} else if (this._previewHandlers[mimePart]) {
return this._previewHandlers[mimePart];
} else {
return null;
}
},
getPreviewHandler: function (mime) {
var mimetypeHandler = this.getMimeTypePreviewHandler(mime);
if (mimetypeHandler) {
return mimetypeHandler;
} else {
return this.fallbackPreview.bind(this);
}
},
loadPreview: function (model, $thumbnailDiv, $thumbnailContainer) {
if (model.get('hasPreview') === false && this.getMimeTypePreviewHandler(model.get('mimetype')) === null) {
var mimeIcon = OC.MimeType.getIconUrl(model.get('mimetype'));
$thumbnailDiv.removeClass('icon-loading icon-32');
$thumbnailContainer.removeClass('image'); //fall back to regular view
$thumbnailDiv.css({
'background-image': 'url("' + mimeIcon + '")'
});
} else {
var handler = this.getPreviewHandler(model.get('mimetype'));
var fallback = this.fallbackPreview.bind(this, model, $thumbnailDiv, $thumbnailContainer);
handler(model, $thumbnailDiv, $thumbnailContainer, fallback);
}
},
// previews for images and mimetype icons
fallbackPreview: function (model, $thumbnailDiv, $thumbnailContainer) {
var isImage = model.isImage();
var maxImageWidth = $thumbnailContainer.parent().width() + 50; // 50px for negative margins
var maxImageHeight = maxImageWidth / (16 / 9);
var isLandscape = function (img) {
return img.width > (img.height * 1.2);
};
var isSmall = function (img) {
return (img.width * 1.1) < (maxImageWidth * window.devicePixelRatio);
};
var getTargetHeight = function (img) {
var targetHeight = img.height / window.devicePixelRatio;
if (targetHeight <= maxImageHeight) {
targetHeight = maxImageHeight;
}
return targetHeight;
};
var getTargetRatio = function (img) {
var ratio = img.width / img.height;
if (ratio > 16 / 9) {
return ratio;
} else {
return 16 / 9;
}
};
this._fileList.lazyLoadPreview({
fileId: model.get('id'),
path: model.getFullPath(),
mime: model.get('mimetype'),
etag: model.get('etag'),
y: maxImageHeight,
x: maxImageWidth,
a: 1,
mode: 'cover',
callback: function (previewUrl, img) {
$thumbnailDiv.previewImg = previewUrl;
// as long as we only have the mimetype icon, we only save it in case there is no preview
if (!img) {
return;
}
$thumbnailDiv.removeClass('icon-loading icon-32');
var targetHeight = getTargetHeight(img);
$thumbnailContainer.addClass((isLandscape(img) && !isSmall(img)) ? 'landscape' : 'portrait');
$thumbnailContainer.addClass('large');
// only set background when we have an actual preview
// when we don't have a preview we show the mime icon in the error handler
$thumbnailDiv.css({
'background-image': 'url("' + previewUrl + '")',
height: (targetHeight > maxImageHeight) ? 'auto' : targetHeight,
'max-height': isSmall(img) ? targetHeight : null
});
var targetRatio = getTargetRatio(img);
$thumbnailDiv.find('.stretcher').css({
'padding-bottom': (100 / targetRatio) + '%'
});
},
error: function () {
$thumbnailDiv.removeClass('icon-loading icon-32');
$thumbnailContainer.removeClass('image'); //fall back to regular view
$thumbnailDiv.css({
'background-image': 'url("' + $thumbnailDiv.previewImg + '")'
});
}
});
}
};
OCA.Files.SidebarPreviewManager = SidebarPreviewManager;
})();
/*
* Copyright (c) 2016
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function () {
var SidebarPreview = function () {
};
SidebarPreview.prototype = {
attach: function (manager) {
manager.addPreviewHandler('text', this.handlePreview.bind(this));
},
handlePreview: function (model, $thumbnailDiv, $thumbnailContainer, fallback) {
var previewWidth = $thumbnailContainer.parent().width() + 50; // 50px for negative margins
var previewHeight = previewWidth / (16 / 9);
this.getFileContent(model.getFullPath()).then(function (content) {
$thumbnailDiv.removeClass('icon-loading icon-32');
$thumbnailContainer.addClass('large');
$thumbnailContainer.addClass('text');
var $textPreview = $('<pre/>').text(content);
$thumbnailDiv.children('.stretcher').remove();
$thumbnailDiv.append($textPreview);
$thumbnailContainer.css("max-height", previewHeight);
}, function () {
fallback();
});
},
getFileContent: function (path) {
return $.ajax({
url: OC.linkToRemoteBase('files' + path),
headers: {
'Range': 'bytes=0-10240'
}
});
}
};
OC.Plugins.register('OCA.Files.SidebarPreviewManager', new SidebarPreview());
})();
/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* @class OCA.Files.DetailTabView
* @classdesc
*
* Base class for tab views to display file information.
*
*/
var DetailTabView = OC.Backbone.View.extend({
tag: 'div',
className: 'tab',
/**
* Tab label
*/
_label: null,
_template: null,
initialize: function(options) {
options = options || {};
if (!this.id) {
this.id = 'detailTabView' + DetailTabView._TAB_COUNT;
DetailTabView._TAB_COUNT++;
}
if (options.order) {
this.order = options.order || 0;
}
},
/**
* Returns the extra CSS classes used by the tabs container when this
* tab is the selected one.
*
* In general you should not extend this method, as tabs should not
* modify the classes of its container; this is reserved as a last
* resort for very specific cases in which there is no other way to get
* the proper style or behaviour.
*
* @return {String} space-separated CSS classes
*/
getTabsContainerExtraClasses: function() {
return '';
},
/**
* Returns the tab label
*
* @return {String} label
*/
getLabel: function() {
return 'Tab ' + this.id;
},
/**
* Returns the tab label
*
* @return {String}|{null} icon class
*/
getIcon: function() {
return null
},
/**
* returns the jQuery object for HTML output
*
* @returns {jQuery}
*/
get$: function() {
return this.$el;
},
/**
* Renders this details view
*
* @abstract
*/
render: function() {
// to be implemented in subclass
// FIXME: code is only for testing
this.$el.html('<div>Hello ' + this.id + '</div>');
},
/**
* Sets the file info to be displayed in the view
*
* @param {OCA.Files.FileInfoModel} fileInfo file info to set
*/
setFileInfo: function(fileInfo) {
if (this.model !== fileInfo) {
this.model = fileInfo;
this.render();
}
},
/**
* Returns the file info.
*
* @return {OCA.Files.FileInfoModel} file info
*/
getFileInfo: function() {
return this.model;
},
/**
* Load the next page of results
*/
nextPage: function() {
// load the next page, if applicable
},
/**
* Returns whether the current tab is able to display
* the given file info, for example based on mime type.
*
* @param {OCA.Files.FileInfoModel} fileInfo file info model
* @return {bool} whether to display this tab
*/
canDisplay: function(fileInfo) {
return true;
}
});
DetailTabView._TAB_COUNT = 0;
OCA.Files = OCA.Files || {};
OCA.Files.DetailTabView = DetailTabView;
})();
/*
* Copyright (c) 2018
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function(){
var Semaphore = function(max) {
var counter = 0;
var waiting = [];
this.acquire = function() {
if(counter < max) {
counter++;
return new Promise(function(resolve) { resolve(); });
} else {
return new Promise(function(resolve) { waiting.push(resolve); });
}
};
this.release = function() {
counter--;
if (waiting.length > 0 && counter < max) {
counter++;
var promise = waiting.shift();
promise();
}
};
};
// needed on public share page to properly register this
if (!OCA.Files) {
OCA.Files = {};
}
OCA.Files.Semaphore = Semaphore;
})();
/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* @class OCA.Files.MainFileInfoDetailView
* @classdesc
*
* Displays main details about a file
*
*/
var MainFileInfoDetailView = OCA.Files.DetailFileInfoView.extend(
/** @lends OCA.Files.MainFileInfoDetailView.prototype */ {
className: 'mainFileInfoView',
/**
* Associated file list instance, for file actions
*
* @type {OCA.Files.FileList}
*/
_fileList: null,
/**
* File actions
*
* @type {OCA.Files.FileActions}
*/
_fileActions: null,
/**
* @type {OCA.Files.SidebarPreviewManager}
*/
_previewManager: null,
events: {
'click a.action-favorite': '_onClickFavorite',
'click a.action-default': '_onClickDefaultAction',
'click a.permalink': '_onClickPermalink',
'focus .permalink-field>input': '_onFocusPermalink'
},
template: function(data) {
return OCA.Files.Templates['mainfileinfodetailsview'](data);
},
initialize: function(options) {
options = options || {};
this._fileList = options.fileList;
this._fileActions = options.fileActions;
if (!this._fileList) {
throw 'Missing required parameter "fileList"';
}
if (!this._fileActions) {
throw 'Missing required parameter "fileActions"';
}
this._previewManager = new OCA.Files.SidebarPreviewManager(this._fileList);
this._setupClipboard();
},
_setupClipboard: function() {
var clipboard = new Clipboard('.permalink');
clipboard.on('success', function(e) {
var $el = $(e.trigger);
$el.tooltip('hide')
.attr('data-original-title', t('core', 'Copied!'))
.tooltip('fixTitle')
.tooltip({placement: 'bottom', trigger: 'manual'})
.tooltip('show');
_.delay(function() {
$el.tooltip('hide');
$el.attr('data-original-title', t('files', 'Copy direct link (only works for users who have access to this file/folder)'))
.tooltip('fixTitle');
}, 3000);
});
clipboard.on('error', function(e) {
var $row = this.$('.permalink-field');
$row.toggleClass('hidden');
if (!$row.hasClass('hidden')) {
$row.find('>input').focus();
}
});
},
_onClickPermalink: function(e) {
e.preventDefault();
return;
},
_onFocusPermalink: function() {
this.$('.permalink-field>input').select();
},
_onClickFavorite: function(event) {
event.preventDefault();
this._fileActions.triggerAction('Favorite', this.model, this._fileList);
},
_onClickDefaultAction: function(event) {
event.preventDefault();
this._fileActions.triggerAction(null, this.model, this._fileList);
},
_onModelChanged: function() {
// simply re-render
this.render();
},
_makePermalink: function(fileId) {
var baseUrl = OC.getProtocol() + '://' + OC.getHost();
return baseUrl + OC.generateUrl('/f/{fileId}', {fileId: fileId});
},
setFileInfo: function(fileInfo) {
if (this.model) {
this.model.off('change', this._onModelChanged, this);
}
this.model = fileInfo;
if (this.model) {
this.model.on('change', this._onModelChanged, this);
}
if (this.model) {
var properties = [];
if( !this.model.has('size') ) {
properties.push(OC.Files.Client.PROPERTY_SIZE);
properties.push(OC.Files.Client.PROPERTY_GETCONTENTLENGTH);
}
if( properties.length > 0){
this.model.reloadProperties(properties);
}
}
this.render();
},
/**
* Renders this details view
*/
render: function() {
this.trigger('pre-render');
if (this.model) {
var isFavorite = (this.model.get('tags') || []).indexOf(OC.TAG_FAVORITE) >= 0;
var availableActions = this._fileActions.get(
this.model.get('mimetype'),
this.model.get('type'),
this.model.get('permissions')
);
var hasFavoriteAction = 'Favorite' in availableActions;
this.$el.html(this.template({
type: this.model.isImage()? 'image': '',
nameLabel: t('files', 'Name'),
name: this.model.get('displayName') || this.model.get('name'),
pathLabel: t('files', 'Path'),
path: this.model.get('path'),
hasSize: this.model.has('size'),
sizeLabel: t('files', 'Size'),
size: OC.Util.humanFileSize(this.model.get('size'), true),
altSize: n('files', '%n byte', '%n bytes', this.model.get('size')),
dateLabel: t('files', 'Modified'),
altDate: OC.Util.formatDate(this.model.get('mtime')),
timestamp: this.model.get('mtime'),
date: OC.Util.relativeModifiedDate(this.model.get('mtime')),
hasFavoriteAction: hasFavoriteAction,
starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'),
starClass: isFavorite ? 'icon-starred' : 'icon-star',
permalink: this._makePermalink(this.model.get('id')),
permalinkTitle: t('files', 'Copy direct link (only works for users who have access to this file/folder)')
}));
// TODO: we really need OC.Previews
var $iconDiv = this.$el.find('.thumbnail');
var $container = this.$el.find('.thumbnailContainer');
if (!this.model.isDirectory()) {
$iconDiv.addClass('icon-loading icon-32');
this._previewManager.loadPreview(this.model, $iconDiv, $container);
} else {
var iconUrl = this.model.get('icon') || OC.MimeType.getIconUrl('dir');
if (typeof this.model.get('mountType') !== 'undefined') {
iconUrl = OC.MimeType.getIconUrl('dir-' + this.model.get('mountType'))
}
$iconDiv.css('background-image', 'url("' + iconUrl + '")');
}
this.$el.find('[title]').tooltip({placement: 'bottom'});
} else {
this.$el.empty();
}
this.delegateEvents();
this.trigger('post-render');
}
});
OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView;
})();
/*
* Copyright (c) 2018
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
var OperationProgressBar = OC.Backbone.View.extend({
tagName: 'div',
id: 'uploadprogresswrapper',
events: {
'click button.stop': '_onClickCancel'
},
render: function() {
this.$el.html(OCA.Files.Templates['operationprogressbar']({
textCancelButton: t('Cancel operation')
}));
this.setProgressBarText(t('Uploading …'), t('…'));
},
hideProgressBar: function() {
var self = this;
$('#uploadprogresswrapper .stop').fadeOut();
$('#uploadprogressbar').fadeOut(function() {
self.$el.trigger(new $.Event('resized'));
});
},
hideCancelButton: function() {
var self = this;
$('#uploadprogresswrapper .stop').fadeOut(function() {
self.$el.trigger(new $.Event('resized'));
});
},
showProgressBar: function(showCancelButton) {
if (showCancelButton) {
showCancelButton = true;
}
$('#uploadprogressbar').progressbar({value: 0});
if(showCancelButton) {
$('#uploadprogresswrapper .stop').show();
} else {
$('#uploadprogresswrapper .stop').hide();
}
$('#uploadprogresswrapper .label').show();
$('#uploadprogressbar').fadeIn();
this.$el.trigger(new $.Event('resized'));
},
setProgressBarValue: function(value) {
$('#uploadprogressbar').progressbar({value: value});
},
setProgressBarText: function(textDesktop, textMobile, title) {
var labelHtml = OCA.Files.Templates['operationprogressbarlabel']({textDesktop: textDesktop, textMobile: textMobile});
$('#uploadprogressbar .ui-progressbar-value').html(labelHtml);
$('#uploadprogressbar .ui-progressbar-value>em').addClass('inner');
$('#uploadprogressbar>em').replaceWith(labelHtml);
$('#uploadprogressbar>em').addClass('outer');
$('#uploadprogressbar').tooltip({placement: 'bottom'});
if(title) {
$('#uploadprogressbar').attr('original-title', title);
}
if(textDesktop || textMobile) {
$('#uploadprogresswrapper .stop').show();
}
},
_onClickCancel: function (event) {
this.trigger('cancel');
return false;
}
});
OCA.Files.OperationProgressBar = OperationProgressBar;
})(OC, OCA);
/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* @class OCA.Files.DetailsView
* @classdesc
*
* The details view show details about a selected file.
*
*/
var DetailsView = OC.Backbone.View.extend({
id: 'app-sidebar',
tabName: 'div',
className: 'detailsView scroll-container',
/**
* List of detail tab views
*
* @type Array<OCA.Files.DetailTabView>
*/
_tabViews: [],
/**
* List of detail file info views
*
* @type Array<OCA.Files.DetailFileInfoView>
*/
_detailFileInfoViews: [],
/**
* Id of the currently selected tab
*
* @type string
*/
_currentTabId: null,
/**
* Dirty flag, whether the view needs to be rerendered
*/
_dirty: false,
events: {
'click a.close': '_onClose',
'click .tabHeaders .tabHeader': '_onClickTab',
'keyup .tabHeaders .tabHeader': '_onKeyboardActivateTab'
},
/**
* Initialize the details view
*/
initialize: function() {
this._tabViews = [];
this._detailFileInfoViews = [];
this._dirty = true;
},
_onClose: function(event) {
OC.Apps.hideAppSidebar(this.$el);
event.preventDefault();
},
_onClickTab: function(e) {
var $target = $(e.target);
e.preventDefault();
if (!$target.hasClass('tabHeader')) {
$target = $target.closest('.tabHeader');
}
var tabId = $target.attr('data-tabid');
if (_.isUndefined(tabId)) {
return;
}
this.selectTab(tabId);
},
_onKeyboardActivateTab: function (event) {
if (event.key === " " || event.key === "Enter") {
this._onClickTab(event);
}
},
template: function(vars) {
return OCA.Files.Templates['detailsview'](vars);
},
/**
* Renders this details view
*/
render: function() {
var templateVars = {
closeLabel: t('files', 'Close')
};
this._tabViews = this._tabViews.sort(function(tabA, tabB) {
var orderA = tabA.order || 0;
var orderB = tabB.order || 0;
if (orderA === orderB) {
return OC.Util.naturalSortCompare(tabA.getLabel(), tabB.getLabel());
}
return orderA - orderB;
});
templateVars.tabHeaders = _.map(this._tabViews, function(tabView, i) {
return {
tabId: tabView.id,
label: tabView.getLabel(),
tabIcon: tabView.getIcon()
};
});
this.$el.html(this.template(templateVars));
var $detailsContainer = this.$el.find('.detailFileInfoContainer');
// render details
_.each(this._detailFileInfoViews, function(detailView) {
$detailsContainer.append(detailView.get$());
});
if (!this._currentTabId && this._tabViews.length > 0) {
this._currentTabId = this._tabViews[0].id;
}
this.selectTab(this._currentTabId);
this._updateTabVisibilities();
this._dirty = false;
},
/**
* Selects the given tab by id
*
* @param {string} tabId tab id
*/
selectTab: function(tabId) {
if (!tabId) {
return;
}
var tabView = _.find(this._tabViews, function(tab) {
return tab.id === tabId;
});
if (!tabView) {
console.warn('Details view tab with id "' + tabId + '" not found');
return;
}
this._currentTabId = tabId;
var $tabsContainer = this.$el.find('.tabsContainer');
var $tabEl = $tabsContainer.find('#' + tabId);
// hide other tabs
$tabsContainer.find('.tab').addClass('hidden');
$tabsContainer.attr('class', 'tabsContainer');
$tabsContainer.addClass(tabView.getTabsContainerExtraClasses());
// tab already rendered ?
if (!$tabEl.length) {
// render tab
$tabsContainer.append(tabView.$el);
$tabEl = tabView.$el;
}
// this should trigger tab rendering
tabView.setFileInfo(this.model);
$tabEl.removeClass('hidden');
// update tab headers
var $tabHeaders = this.$el.find('.tabHeaders li');
$tabHeaders.removeClass('selected');
$tabHeaders.filterAttr('data-tabid', tabView.id).addClass('selected');
},
/**
* Sets the file info to be displayed in the view
*
* @param {OCA.Files.FileInfoModel} fileInfo file info to set
*/
setFileInfo: function(fileInfo) {
this.model = fileInfo;
if (this._dirty) {
this.render();
} else {
this._updateTabVisibilities();
}
if (this._currentTabId) {
// only update current tab, others will be updated on-demand
var tabId = this._currentTabId;
var tabView = _.find(this._tabViews, function(tab) {
return tab.id === tabId;
});
tabView.setFileInfo(fileInfo);
}
_.each(this._detailFileInfoViews, function(detailView) {
detailView.setFileInfo(fileInfo);
});
},
/**
* Update tab headers based on the current model
*/
_updateTabVisibilities: function() {
// update tab header visibilities
var self = this;
var deselect = false;
var countVisible = 0;
var $tabHeaders = this.$el.find('.tabHeaders li');
_.each(this._tabViews, function(tabView) {
var isVisible = tabView.canDisplay(self.model);
if (isVisible) {
countVisible += 1;
}
if (!isVisible && self._currentTabId === tabView.id) {
deselect = true;
}
$tabHeaders.filterAttr('data-tabid', tabView.id).toggleClass('hidden', !isVisible);
});
// hide the whole container if there is only one tab
this.$el.find('.tabHeaders').toggleClass('hidden', countVisible <= 1);
if (deselect) {
// select the first visible tab instead
var visibleTabId = this.$el.find('.tabHeader:not(.hidden):first').attr('data-tabid');
this.selectTab(visibleTabId);
}
},
/**
* Returns the file info.
*
* @return {OCA.Files.FileInfoModel} file info
*/
getFileInfo: function() {
return this.model;
},
/**
* Adds a tab in the tab view
*
* @param {OCA.Files.DetailTabView} tab view
*/
addTabView: function(tabView) {
this._tabViews.push(tabView);
this._dirty = true;
},
/**
* Adds a detail view for file info.
*
* @param {OCA.Files.DetailFileInfoView} detail view
*/
addDetailView: function(detailView) {
this._detailFileInfoViews.push(detailView);
this._dirty = true;
},
/**
* Returns an array with the added DetailFileInfoViews.
*
* @return Array<OCA.Files.DetailFileInfoView> an array with the added
* DetailFileInfoViews.
*/
getDetailViews: function() {
return [].concat(this._detailFileInfoViews);
}
});
OCA.Files.DetailsView = DetailsView;
})();
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* Construct a new FileActions instance
* @constructs FileActions
* @memberof OCA.Files
*/
var FileActions = function() {
this.initialize();
};
FileActions.TYPE_DROPDOWN = 0;
FileActions.TYPE_INLINE = 1;
FileActions.prototype = {
/** @lends FileActions.prototype */
actions: {},
defaults: {},
icons: {},
/**
* @deprecated
*/
currentFile: null,
/**
* Dummy jquery element, for events
*/
$el: null,
_fileActionTriggerTemplate: null,
/**
* @private
*/
initialize: function() {
this.clear();
// abusing jquery for events until we get a real event lib
this.$el = $('<div class="dummy-fileactions hidden"></div>');
$('body').append(this.$el);
this._showMenuClosure = _.bind(this._showMenu, this);
},
/**
* Adds an event handler
*
* @param {String} eventName event name
* @param {Function} callback
*/
on: function(eventName, callback) {
this.$el.on(eventName, callback);
},
/**
* Removes an event handler
*
* @param {String} eventName event name
* @param {Function} callback
*/
off: function(eventName, callback) {
this.$el.off(eventName, callback);
},
/**
* Notifies the event handlers
*
* @param {String} eventName event name
* @param {Object} data data
*/
_notifyUpdateListeners: function(eventName, data) {
this.$el.trigger(new $.Event(eventName, data));
},
/**
* Merges the actions from the given fileActions into
* this instance.
*
* @param {OCA.Files.FileActions} fileActions instance of OCA.Files.FileActions
*/
merge: function(fileActions) {
var self = this;
// merge first level to avoid unintended overwriting
_.each(fileActions.actions, function(sourceMimeData, mime) {
var targetMimeData = self.actions[mime];
if (!targetMimeData) {
targetMimeData = {};
}
self.actions[mime] = _.extend(targetMimeData, sourceMimeData);
});
this.defaults = _.extend(this.defaults, fileActions.defaults);
this.icons = _.extend(this.icons, fileActions.icons);
},
/**
* @deprecated use #registerAction() instead
*/
register: function(mime, name, permissions, icon, action, displayName) {
return this.registerAction({
name: name,
mime: mime,
permissions: permissions,
icon: icon,
actionHandler: action,
displayName: displayName || name
});
},
/**
* Register action
*
* @param {OCA.Files.FileAction} action object
*/
registerAction: function (action) {
var mime = action.mime;
var name = action.name;
var actionSpec = {
action: function(fileName, context) {
// Actions registered in one FileAction may be executed on a
// different one (for example, due to the "merge" function),
// so the listeners have to be updated on the FileActions
// from the context instead of on the one in which it was
// originally registered.
if (context && context.fileActions) {
context.fileActions._notifyUpdateListeners('beforeTriggerAction', {action: actionSpec, fileName: fileName, context: context});
}
action.actionHandler(fileName, context);
if (context && context.fileActions) {
context.fileActions._notifyUpdateListeners('afterTriggerAction', {action: actionSpec, fileName: fileName, context: context});
}
},
name: name,
displayName: action.displayName,
mime: mime,
order: action.order || 0,
icon: action.icon,
iconClass: action.iconClass,
permissions: action.permissions,
type: action.type || FileActions.TYPE_DROPDOWN,
altText: action.altText || ''
};
if (_.isUndefined(action.displayName)) {
actionSpec.displayName = t('files', name);
}
if (_.isFunction(action.render)) {
actionSpec.render = action.render;
}
if (!this.actions[mime]) {
this.actions[mime] = {};
}
this.actions[mime][name] = actionSpec;
this.icons[name] = action.icon;
this._notifyUpdateListeners('registerAction', {action: action});
},
/**
* Clears all registered file actions.
*/
clear: function() {
this.actions = {};
this.defaults = {};
this.icons = {};
this.currentFile = null;
},
/**
* Sets the default action for a given mime type.
*
* @param {String} mime mime type
* @param {String} name action name
*/
setDefault: function (mime, name) {
this.defaults[mime] = name;
this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}});
},
/**
* Returns a map of file actions handlers matching the given conditions
*
* @param {string} mime mime type
* @param {string} type "dir" or "file"
* @param {int} permissions permissions
*
* @return {Object.<string,OCA.Files.FileActions~actionHandler>} map of action name to action spec
*/
get: function (mime, type, permissions) {
var actions = this.getActions(mime, type, permissions);
var filteredActions = {};
$.each(actions, function (name, action) {
filteredActions[name] = action.action;
});
return filteredActions;
},
/**
* Returns an array of file actions matching the given conditions
*
* @param {string} mime mime type
* @param {string} type "dir" or "file"
* @param {int} permissions permissions
*
* @return {Array.<OCA.Files.FileAction>} array of action specs
*/
getActions: function (mime, type, permissions) {
var actions = {};
if (this.actions.all) {
actions = $.extend(actions, this.actions.all);
}
if (type) {//type is 'dir' or 'file'
if (this.actions[type]) {
actions = $.extend(actions, this.actions[type]);
}
}
if (mime) {
var mimePart = mime.substr(0, mime.indexOf('/'));
if (this.actions[mimePart]) {
actions = $.extend(actions, this.actions[mimePart]);
}
if (this.actions[mime]) {
actions = $.extend(actions, this.actions[mime]);
}
}
var filteredActions = {};
$.each(actions, function (name, action) {
if ((action.permissions === OC.PERMISSION_NONE) || (action.permissions & permissions)) {
filteredActions[name] = action;
}
});
return filteredActions;
},
/**
* Returns the default file action handler for the given conditions
*
* @param {string} mime mime type
* @param {string} type "dir" or "file"
* @param {int} permissions permissions
*
* @return {OCA.Files.FileActions~actionHandler} action handler
*
* @deprecated use getDefaultFileAction instead
*/
getDefault: function (mime, type, permissions) {
var defaultActionSpec = this.getDefaultFileAction(mime, type, permissions);
if (defaultActionSpec) {
return defaultActionSpec.action;
}
return undefined;
},
/**
* Returns the default file action handler for the given conditions
*
* @param {string} mime mime type
* @param {string} type "dir" or "file"
* @param {int} permissions permissions
*
* @return {OCA.Files.FileActions~actionHandler} action handler
* @since 8.2
*/
getDefaultFileAction: function(mime, type, permissions) {
var mimePart;
if (mime) {
mimePart = mime.substr(0, mime.indexOf('/'));
}
var name = false;
if (mime && this.defaults[mime]) {
name = this.defaults[mime];
} else if (mime && this.defaults[mimePart]) {
name = this.defaults[mimePart];
} else if (type && this.defaults[type]) {
name = this.defaults[type];
} else {
name = this.defaults.all;
}
var actions = this.getActions(mime, type, permissions);
return actions[name];
},
/**
* Default function to render actions
*
* @param {OCA.Files.FileAction} actionSpec file action spec
* @param {boolean} isDefault true if the action is a default one,
* false otherwise
* @param {OCA.Files.FileActionContext} context action context
*/
_defaultRenderAction: function(actionSpec, isDefault, context) {
if (!isDefault) {
var params = {
name: actionSpec.name,
nameLowerCase: actionSpec.name.toLowerCase(),
displayName: actionSpec.displayName,
icon: actionSpec.icon,
iconClass: actionSpec.iconClass,
altText: actionSpec.altText,
hasDisplayName: !!actionSpec.displayName
};
if (_.isFunction(actionSpec.icon)) {
params.icon = actionSpec.icon(context.$file.attr('data-file'), context);
}
if (_.isFunction(actionSpec.iconClass)) {
params.iconClass = actionSpec.iconClass(context.$file.attr('data-file'), context);
}
var $actionLink = this._makeActionLink(params, context);
context.$file.find('a.name>span.fileactions').append($actionLink);
$actionLink.addClass('permanent');
return $actionLink;
}
},
/**
* Renders the action link element
*
* @param {Object} params action params
*/
_makeActionLink: function(params) {
return $(OCA.Files.Templates['file_action_trigger'](params));
},
/**
* Displays the file actions dropdown menu
*
* @param {string} fileName file name
* @param {OCA.Files.FileActionContext} context rendering context
*/
_showMenu: function(fileName, context) {
var menu;
var $trigger = context.$file.closest('tr').find('.fileactions .action-menu');
$trigger.addClass('open');
menu = new OCA.Files.FileActionsMenu();
context.$file.find('td.filename').append(menu.$el);
menu.$el.on('afterHide', function() {
context.$file.removeClass('mouseOver');
$trigger.removeClass('open');
menu.remove();
});
context.$file.addClass('mouseOver');
menu.show(context);
},
/**
* Renders the menu trigger on the given file list row
*
* @param {Object} $tr file list row element
* @param {OCA.Files.FileActionContext} context rendering context
*/
_renderMenuTrigger: function($tr, context) {
// remove previous
$tr.find('.action-menu').remove();
var $el = this._renderInlineAction({
name: 'menu',
displayName: '',
iconClass: 'icon-more',
altText: t('files', 'Actions'),
action: this._showMenuClosure
}, false, context);
$el.addClass('permanent');
},
/**
* Renders the action element by calling actionSpec.render() and
* registers the click event to process the action.
*
* @param {OCA.Files.FileAction} actionSpec file action to render
* @param {boolean} isDefault true if the action is a default action,
* false otherwise
* @param {OCA.Files.FileActionContext} context rendering context
*/
_renderInlineAction: function(actionSpec, isDefault, context) {
var renderFunc = actionSpec.render || _.bind(this._defaultRenderAction, this);
var $actionEl = renderFunc(actionSpec, isDefault, context);
if (!$actionEl || !$actionEl.length) {
return;
}
$actionEl.on(
'click', {
a: null
},
function(event) {
event.stopPropagation();
event.preventDefault();
if ($actionEl.hasClass('open')) {
return;
}
var $file = $(event.target).closest('tr');
if ($file.hasClass('busy')) {
return;
}
var currentFile = $file.find('td.filename');
var fileName = $file.attr('data-file');
context.fileActions.currentFile = currentFile;
// also set on global object for legacy apps
window.FileActions.currentFile = currentFile;
var callContext = _.extend({}, context);
if (!context.dir && context.fileList) {
callContext.dir = $file.attr('data-path') || context.fileList.getCurrentDirectory();
}
if (!context.fileInfoModel && context.fileList) {
callContext.fileInfoModel = context.fileList.getModelForFile(fileName);
if (!callContext.fileInfoModel) {
console.warn('No file info model found for file "' + fileName + '"');
}
}
actionSpec.action(
fileName,
callContext
);
}
);
$actionEl.tooltip({placement:'top'});
return $actionEl;
},
/**
* Trigger the given action on the given file.
*
* @param {string} actionName action name
* @param {OCA.Files.FileInfoModel} fileInfoModel file info model
* @param {OCA.Files.FileList} [fileList] file list, for compatibility with older action handlers [DEPRECATED]
*
* @return {boolean} true if the action handler was called, false otherwise
*
* @since 8.2
*/
triggerAction: function(actionName, fileInfoModel, fileList) {
var actionFunc;
var actions = this.get(
fileInfoModel.get('mimetype'),
fileInfoModel.isDirectory() ? 'dir' : 'file',
fileInfoModel.get('permissions')
);
if (actionName) {
actionFunc = actions[actionName];
} else {
actionFunc = this.getDefault(
fileInfoModel.get('mimetype'),
fileInfoModel.isDirectory() ? 'dir' : 'file',
fileInfoModel.get('permissions')
);
}
if (!actionFunc) {
actionFunc = actions['Download'];
}
if (!actionFunc) {
return false;
}
var context = {
fileActions: this,
fileInfoModel: fileInfoModel,
dir: fileInfoModel.get('path')
};
var fileName = fileInfoModel.get('name');
this.currentFile = fileName;
// also set on global object for legacy apps
window.FileActions.currentFile = fileName;
if (fileList) {
// compatibility with action handlers that expect these
context.fileList = fileList;
context.$file = fileList.findFileEl(fileName);
}
actionFunc(fileName, context);
},
/**
* Display file actions for the given element
* @param parent "td" element of the file for which to display actions
* @param triggerEvent if true, triggers the fileActionsReady on the file
* list afterwards (false by default)
* @param fileList OCA.Files.FileList instance on which the action is
* done, defaults to OCA.Files.App.fileList
*/
display: function (parent, triggerEvent, fileList) {
if (!fileList) {
console.warn('FileActions.display() MUST be called with a OCA.Files.FileList instance');
return;
}
this.currentFile = parent;
var self = this;
var $tr = parent.closest('tr');
var actions = this.getActions(
this.getCurrentMimeType(),
this.getCurrentType(),
this.getCurrentPermissions()
);
var nameLinks;
if ($tr.data('renaming')) {
return;
}
// recreate fileactions container
nameLinks = parent.children('a.name');
nameLinks.find('.fileactions, .nametext .action').remove();
nameLinks.append('<span class="fileactions" />');
var defaultAction = this.getDefaultFileAction(
this.getCurrentMimeType(),
this.getCurrentType(),
this.getCurrentPermissions()
);
var context = {
$file: $tr,
fileActions: this,
fileList: fileList
};
$.each(actions, function (name, actionSpec) {
if (actionSpec.type === FileActions.TYPE_INLINE) {
self._renderInlineAction(
actionSpec,
defaultAction && actionSpec.name === defaultAction.name,
context
);
}
});
function objectValues(obj) {
var res = [];
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
res.push(obj[i]);
}
}
return res;
}
// polyfill
if (!Object.values) {
Object.values = objectValues;
}
var menuActions = Object.values(this.actions.all).filter(function (action) {
return action.type !== OCA.Files.FileActions.TYPE_INLINE;
});
// do not render the menu if nothing is in it
if (menuActions.length > 0) {
this._renderMenuTrigger($tr, context);
}
if (triggerEvent){
fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr}));
}
},
getCurrentFile: function () {
return this.currentFile.parent().attr('data-file');
},
getCurrentMimeType: function () {
return this.currentFile.parent().attr('data-mime');
},
getCurrentType: function () {
return this.currentFile.parent().attr('data-type');
},
getCurrentPermissions: function () {
return this.currentFile.parent().data('permissions');
},
/**
* Register the actions that are used by default for the files app.
*/
registerDefaultActions: function() {
this.registerAction({
name: 'Download',
displayName: t('files', 'Download'),
order: -20,
mime: 'all',
permissions: OC.PERMISSION_READ,
iconClass: 'icon-download',
actionHandler: function (filename, context) {
var dir = context.dir || context.fileList.getCurrentDirectory();
var isDir = context.$file.attr('data-type') === 'dir';
var url = context.fileList.getDownloadUrl(filename, dir, isDir);
var downloadFileaction = $(context.$file).find('.fileactions .action-download');
// don't allow a second click on the download action
if(downloadFileaction.hasClass('disabled')) {
return;
}
if (url) {
var disableLoadingState = function() {
context.fileList.showFileBusyState(filename, false);
};
context.fileList.showFileBusyState(filename, true);
OCA.Files.Files.handleDownload(url, disableLoadingState);
}
}
});
this.registerAction({
name: 'Rename',
displayName: t('files', 'Rename'),
mime: 'all',
order: -30,
permissions: OC.PERMISSION_UPDATE,
iconClass: 'icon-rename',
actionHandler: function (filename, context) {
context.fileList.rename(filename);
}
});
this.registerAction({
name: 'MoveCopy',
displayName: function(context) {
var permissions = context.fileInfoModel.attributes.permissions;
if (permissions & OC.PERMISSION_UPDATE) {
return t('files', 'Move or copy');
}
return t('files', 'Copy');
},
mime: 'all',
order: -25,
permissions: $('#isPublic').val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ,
iconClass: 'icon-external',
actionHandler: function (filename, context) {
var permissions = context.fileInfoModel.attributes.permissions;
var actions = OC.dialogs.FILEPICKER_TYPE_COPY;
if (permissions & OC.PERMISSION_UPDATE) {
actions = OC.dialogs.FILEPICKER_TYPE_COPY_MOVE;
}
var dialogDir = context.dir;
if (typeof context.fileList.dirInfo.dirLastCopiedTo !== 'undefined') {
dialogDir = context.fileList.dirInfo.dirLastCopiedTo;
}
OC.dialogs.filepicker(t('files', 'Choose target folder'), function(targetPath, type) {
if (type === OC.dialogs.FILEPICKER_TYPE_COPY) {
context.fileList.copy(filename, targetPath, false, context.dir);
}
if (type === OC.dialogs.FILEPICKER_TYPE_MOVE) {
context.fileList.move(filename, targetPath, false, context.dir);
}
context.fileList.dirInfo.dirLastCopiedTo = targetPath;
}, false, "httpd/unix-directory", true, actions, dialogDir);
}
});
this.registerAction({
name: 'Open',
mime: 'dir',
permissions: OC.PERMISSION_READ,
icon: '',
actionHandler: function (filename, context) {
var dir = context.$file.attr('data-path') || context.fileList.getCurrentDirectory();
if (OCA.Files.App && OCA.Files.App.getActiveView() !== 'files') {
OCA.Files.App.setActiveView('files', {silent: true});
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(dir, filename), true, true);
} else {
context.fileList.changeDirectory(OC.joinPaths(dir, filename), true, false, parseInt(context.$file.attr('data-id'), 10));
}
},
displayName: t('files', 'Open')
});
this.registerAction({
name: 'Delete',
displayName: function(context) {
var mountType = context.$file.attr('data-mounttype');
var type = context.$file.attr('data-type');
var deleteTitle = (type && type === 'file')
? t('files', 'Delete file')
: t('files', 'Delete folder')
if (mountType === 'external-root') {
deleteTitle = t('files', 'Disconnect storage');
} else if (mountType === 'shared-root') {
deleteTitle = t('files', 'Unshare');
}
return deleteTitle;
},
mime: 'all',
order: 1000,
// permission is READ because we show a hint instead if there is no permission
permissions: OC.PERMISSION_DELETE,
iconClass: 'icon-delete',
actionHandler: function(fileName, context) {
// if there is no permission to delete do nothing
if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) {
return;
}
context.fileList.do_delete(fileName, context.dir);
$('.tipsy').remove();
}
});
this.setDefault('dir', 'Open');
}
};
OCA.Files.FileActions = FileActions;
/**
* Replaces the button icon with a loading spinner and vice versa
* - also adds the class disabled to the passed in element
*
* @param {jQuery} $buttonElement The button element
* @param {boolean} showIt whether to show the spinner(true) or to hide it(false)
*/
OCA.Files.FileActions.updateFileActionSpinner = function($buttonElement, showIt) {
var $icon = $buttonElement.find('.icon');
if (showIt) {
var $loadingIcon = $('<span class="icon icon-loading-small"></span>');
$icon.after($loadingIcon);
$icon.addClass('hidden');
} else {
$buttonElement.find('.icon-loading-small').remove();
$buttonElement.find('.icon').removeClass('hidden');
}
};
/**
* File action attributes.
*
* @todo make this a real class in the future
* @typedef {Object} OCA.Files.FileAction
*
* @property {String} name identifier of the action
* @property {(String|OCA.Files.FileActions~displayNameFunction)} displayName
* display name string for the action, or function that returns the display name.
* Defaults to the name given in name property
* @property {String} mime mime type
* @property {int} permissions permissions
* @property {(Function|String)} icon icon path to the icon or function that returns it (deprecated, use iconClass instead)
* @property {(String|OCA.Files.FileActions~iconClassFunction)} iconClass class name of the icon (recommended for theming)
* @property {OCA.Files.FileActions~renderActionFunction} [render] optional rendering function
* @property {OCA.Files.FileActions~actionHandler} actionHandler action handler function
*/
/**
* File action context attributes.
*
* @typedef {Object} OCA.Files.FileActionContext
*
* @property {Object} $file jQuery file row element
* @property {OCA.Files.FileActions} fileActions file actions object
* @property {OCA.Files.FileList} fileList file list object
*/
/**
* Render function for actions.
* The function must render a link element somewhere in the DOM
* and return it. The function should NOT register the event handler
* as this will be done after the link was returned.
*
* @callback OCA.Files.FileActions~renderActionFunction
* @param {OCA.Files.FileAction} actionSpec action definition
* @param {Object} $row row container
* @param {boolean} isDefault true if the action is the default one,
* false otherwise
* @return {Object} jQuery link object
*/
/**
* Display name function for actions.
* The function returns the display name of the action using
* the given context information..
*
* @callback OCA.Files.FileActions~displayNameFunction
* @param {OCA.Files.FileActionContext} context action context
* @return {String} display name
*/
/**
* Icon class function for actions.
* The function returns the icon class of the action using
* the given context information.
*
* @callback OCA.Files.FileActions~iconClassFunction
* @param {String} fileName name of the file on which the action must be performed
* @param {OCA.Files.FileActionContext} context action context
* @return {String} icon class
*/
/**
* Action handler function for file actions
*
* @callback OCA.Files.FileActions~actionHandler
* @param {String} fileName name of the file on which the action must be performed
* @param context context
* @param {String} context.dir directory of the file
* @param {OCA.Files.FileInfoModel} fileInfoModel file info model
* @param {Object} [context.$file] jQuery element of the file [DEPRECATED]
* @param {OCA.Files.FileList} [context.fileList] the FileList instance on which the action occurred [DEPRECATED]
* @param {OCA.Files.FileActions} context.fileActions the FileActions instance on which the action occurred
*/
// global file actions to be used by all lists
OCA.Files.fileActions = new OCA.Files.FileActions();
OCA.Files.legacyFileActions = new OCA.Files.FileActions();
// for backward compatibility
//
// legacy apps are expecting a stateful global FileActions object to register
// their actions on. Since legacy apps are very likely to break with other
// FileList views than the main one ("All files"), actions registered
// through window.FileActions will be limited to the main file list.
// @deprecated use OCA.Files.FileActions instead
window.FileActions = OCA.Files.legacyFileActions;
window.FileActions.register = function (mime, name, permissions, icon, action, displayName) {
console.warn('FileActions.register() is deprecated, please use OCA.Files.fileActions.register() instead', arguments);
OCA.Files.FileActions.prototype.register.call(
window.FileActions, mime, name, permissions, icon, action, displayName
);
};
window.FileActions.display = function (parent, triggerEvent, fileList) {
fileList = fileList || OCA.Files.App.fileList;
console.warn('FileActions.display() is deprecated, please use OCA.Files.fileActions.register() which automatically redisplays actions', mime, name);
OCA.Files.FileActions.prototype.display.call(window.FileActions, parent, triggerEvent, fileList);
};
})();
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* Construct a new FileActionsMenu instance
* @constructs FileActionsMenu
* @memberof OCA.Files
*/
var FileActionsMenu = OC.Backbone.View.extend({
tagName: 'div',
className: 'fileActionsMenu popovermenu bubble hidden open menu',
/**
* Current context
*
* @type OCA.Files.FileActionContext
*/
_context: null,
events: {
'click a.action': '_onClickAction'
},
template: function(data) {
return OCA.Files.Templates['fileactionsmenu'](data);
},
/**
* Event handler whenever an action has been clicked within the menu
*
* @param {Object} event event object
*/
_onClickAction: function(event) {
var $target = $(event.target);
if (!$target.is('a')) {
$target = $target.closest('a');
}
var fileActions = this._context.fileActions;
var actionName = $target.attr('data-action');
var actions = fileActions.getActions(
fileActions.getCurrentMimeType(),
fileActions.getCurrentType(),
fileActions.getCurrentPermissions()
);
var actionSpec = actions[actionName];
var fileName = this._context.$file.attr('data-file');
event.stopPropagation();
event.preventDefault();
OC.hideMenus();
actionSpec.action(
fileName,
this._context
);
},
/**
* Renders the menu with the currently set items
*/
render: function() {
var self = this;
var fileActions = this._context.fileActions;
var actions = fileActions.getActions(
fileActions.getCurrentMimeType(),
fileActions.getCurrentType(),
fileActions.getCurrentPermissions()
);
var defaultAction = fileActions.getDefaultFileAction(
fileActions.getCurrentMimeType(),
fileActions.getCurrentType(),
fileActions.getCurrentPermissions()
);
var items = _.filter(actions, function(actionSpec) {
return !defaultAction || actionSpec.name !== defaultAction.name;
});
items = _.map(items, function(item) {
if (_.isFunction(item.displayName)) {
item = _.extend({}, item);
item.displayName = item.displayName(self._context);
}
if (_.isFunction(item.iconClass)) {
var fileName = self._context.$file.attr('data-file');
item = _.extend({}, item);
item.iconClass = item.iconClass(fileName, self._context);
}
if (_.isFunction(item.icon)) {
var fileName = self._context.$file.attr('data-file');
item = _.extend({}, item);
item.icon = item.icon(fileName, self._context);
}
item.inline = item.type === OCA.Files.FileActions.TYPE_INLINE
return item;
});
items = items.sort(function(actionA, actionB) {
var orderA = actionA.order || 0;
var orderB = actionB.order || 0;
if (orderB === orderA) {
return OC.Util.naturalSortCompare(actionA.displayName, actionB.displayName);
}
return orderA - orderB;
});
items = _.map(items, function(item) {
item.nameLowerCase = item.name.toLowerCase();
return item;
});
this.$el.html(this.template({
items: items
}));
},
/**
* Displays the menu under the given element
*
* @param {OCA.Files.FileActionContext} context context
* @param {Object} $trigger trigger element
*/
show: function(context) {
this._context = context;
this.render();
this.$el.removeClass('hidden');
OC.showMenu(null, this.$el);
}
});
OCA.Files.FileActionsMenu = FileActionsMenu;
})();
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/* global getURLParameter */
/**
* Utility class for file related operations
*/
(function() {
var Files = {
// file space size sync
_updateStorageStatistics: function(currentDir) {
var state = Files.updateStorageStatistics;
if (state.dir){
if (state.dir === currentDir) {
return;
}
// cancel previous call, as it was for another dir
state.call.abort();
}
state.dir = currentDir;
state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php') + '?dir=' + encodeURIComponent(currentDir),function(response) {
state.dir = null;
state.call = null;
Files.updateMaxUploadFilesize(response);
});
},
// update quota
updateStorageQuotas: function() {
Files._updateStorageQuotasThrottled();
},
_updateStorageQuotas: function() {
var state = Files.updateStorageQuotas;
state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php'),function(response) {
Files.updateQuota(response);
});
},
/**
* Update storage statistics such as free space, max upload,
* etc based on the given directory.
*
* Note this function is debounced to avoid making too
* many ajax calls in a row.
*
* @param dir directory
* @param force whether to force retrieving
*/
updateStorageStatistics: function(dir, force) {
if (!OC.currentUser) {
return;
}
if (force) {
Files._updateStorageStatistics(dir);
}
else {
Files._updateStorageStatisticsDebounced(dir);
}
},
updateMaxUploadFilesize:function(response) {
if (response === undefined) {
return;
}
if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) {
$('#free_space').val(response.data.freeSpace);
$('#upload.button').attr('data-original-title', response.data.maxHumanFilesize);
$('#usedSpacePercent').val(response.data.usedSpacePercent);
$('#owner').val(response.data.owner);
$('#ownerDisplayName').val(response.data.ownerDisplayName);
Files.displayStorageWarnings();
OCA.Files.App.fileList._updateDirectoryPermissions();
}
if (response[0] === undefined) {
return;
}
if (response[0].uploadMaxFilesize !== undefined) {
$('#upload.button').attr('data-original-title', response[0].maxHumanFilesize);
$('#usedSpacePercent').val(response[0].usedSpacePercent);
Files.displayStorageWarnings();
}
},
updateQuota:function(response) {
if (response === undefined) {
return;
}
if (response.data !== undefined
&& response.data.quota !== undefined
&& response.data.used !== undefined
&& response.data.usedSpacePercent !== undefined) {
var humanUsed = OC.Util.humanFileSize(response.data.used, true);
var humanQuota = OC.Util.humanFileSize(response.data.quota, true);
if (response.data.quota > 0) {
$('#quota').attr('data-original-title', Math.floor(response.data.used/response.data.quota*1000)/10 + '%');
$('#quota progress').val(response.data.usedSpacePercent);
$('#quotatext').text(t('files', '{used} of {quota} used', {used: humanUsed, quota: humanQuota}));
} else {
$('#quotatext').text(t('files', '{used} used', {used: humanUsed}));
}
if (response.data.usedSpacePercent > 80) {
$('#quota progress').addClass('warn');
} else {
$('#quota progress').removeClass('warn');
}
}
},
/**
* Fix path name by removing double slash at the beginning, if any
*/
fixPath: function(fileName) {
if (fileName.substr(0, 2) == '//') {
return fileName.substr(1);
}
return fileName;
},
/**
* Checks whether the given file name is valid.
* @param name file name to check
* @return true if the file name is valid.
* Throws a string exception with an error message if
* the file name is not valid
*/
isFileNameValid: function (name) {
var trimmedName = name.trim();
if (trimmedName === '.' || trimmedName === '..')
{
throw t('files', '"{name}" is an invalid file name.', {name: name});
} else if (trimmedName.length === 0) {
throw t('files', 'File name cannot be empty.');
} else if (trimmedName.indexOf('/') !== -1) {
throw t('files', '"/" is not allowed inside a file name.');
} else if (!!(trimmedName.match(OC.config.blacklist_files_regex))) {
throw t('files', '"{name}" is not an allowed filetype', {name: name});
}
return true;
},
displayStorageWarnings: function() {
if (!OC.Notification.isHidden()) {
return;
}
var usedSpacePercent = $('#usedSpacePercent').val(),
owner = $('#owner').val(),
ownerDisplayName = $('#ownerDisplayName').val();
if (usedSpacePercent > 98) {
if (owner !== OC.getCurrentUser().uid) {
OC.Notification.show(t('files', 'Storage of {owner} is full, files can not be updated or synced anymore!',
{owner: ownerDisplayName}), {type: 'error'}
);
return;
}
OC.Notification.show(t('files',
'Your storage is full, files can not be updated or synced anymore!'),
{type : 'error'}
);
return;
}
if (usedSpacePercent > 90) {
if (owner !== OC.getCurrentUser().uid) {
OC.Notification.show(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%)',
{
usedSpacePercent: usedSpacePercent,
owner: ownerDisplayName
}),
{
type: 'error'
}
);
return;
}
OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)',
{usedSpacePercent: usedSpacePercent}),
{type : 'error'}
);
}
},
/**
* Returns the download URL of the given file(s)
* @param {string} filename string or array of file names to download
* @param {string} [dir] optional directory in which the file name is, defaults to the current directory
* @param {bool} [isDir=false] whether the given filename is a directory and might need a special URL
*/
getDownloadUrl: function(filename, dir, isDir) {
if (!_.isArray(filename) && !isDir) {
var pathSections = dir.split('/');
pathSections.push(filename);
var encodedPath = '';
_.each(pathSections, function(section) {
if (section !== '') {
encodedPath += '/' + encodeURIComponent(section);
}
});
return OC.linkToRemoteBase('webdav') + encodedPath;
}
if (_.isArray(filename)) {
filename = JSON.stringify(filename);
}
var params = {
dir: dir,
files: filename
};
return this.getAjaxUrl('download', params);
},
/**
* Returns the ajax URL for a given action
* @param action action string
* @param params optional params map
*/
getAjaxUrl: function(action, params) {
var q = '';
if (params) {
q = '?' + OC.buildQueryString(params);
}
return OC.filePath('files', 'ajax', action + '.php') + q;
},
/**
* Fetch the icon url for the mimetype
* @param {string} mime The mimetype
* @param {Files~mimeicon} ready Function to call when mimetype is retrieved
* @deprecated use OC.MimeType.getIconUrl(mime)
*/
getMimeIcon: function(mime, ready) {
ready(OC.MimeType.getIconUrl(mime));
},
/**
* Generates a preview URL based on the URL space.
* @param urlSpec attributes for the URL
* @param {int} urlSpec.x width
* @param {int} urlSpec.y height
* @param {String} urlSpec.file path to the file
* @return preview URL
* @deprecated used OCA.Files.FileList.generatePreviewUrl instead
*/
generatePreviewUrl: function(urlSpec) {
console.warn('DEPRECATED: please use generatePreviewUrl() from an OCA.Files.FileList instance');
return OCA.Files.App.fileList.generatePreviewUrl(urlSpec);
},
/**
* Lazy load preview
* @deprecated used OCA.Files.FileList.lazyLoadPreview instead
*/
lazyLoadPreview : function(path, mime, ready, width, height, etag) {
console.warn('DEPRECATED: please use lazyLoadPreview() from an OCA.Files.FileList instance');
return FileList.lazyLoadPreview({
path: path,
mime: mime,
callback: ready,
width: width,
height: height,
etag: etag
});
},
/**
* Initialize the files view
*/
initialize: function() {
Files.bindKeyboardShortcuts(document, $);
// TODO: move file list related code (upload) to OCA.Files.FileList
$('#file_action_panel').attr('activeAction', false);
// drag&drop support using jquery.fileupload
// TODO use OC.dialogs
$(document).bind('drop dragover', function (e) {
e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone
});
// display storage warnings
setTimeout(Files.displayStorageWarnings, 100);
// only possible at the moment if user is logged in or the files app is loaded
if (OC.currentUser && OCA.Files.App) {
// start on load - we ask the server every 5 minutes
var func = _.bind(OCA.Files.App.fileList.updateStorageStatistics, OCA.Files.App.fileList);
var updateStorageStatisticsInterval = 5*60*1000;
var updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval);
// TODO: this should also stop when switching to another view
// Use jquery-visibility to de-/re-activate file stats sync
if ($.support.pageVisibility) {
$(document).on({
'show': function() {
if (!updateStorageStatisticsIntervalId) {
updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval);
}
},
'hide': function() {
clearInterval(updateStorageStatisticsIntervalId);
updateStorageStatisticsIntervalId = 0;
}
});
}
}
$('#webdavurl').on('click touchstart', function () {
this.focus();
this.setSelectionRange(0, this.value.length);
});
$('#upload').tooltip({placement:'right'});
//FIXME scroll to and highlight preselected file
/*
if (getURLParameter('scrollto')) {
FileList.scrollTo(getURLParameter('scrollto'));
}
*/
},
/**
* Handles the download and calls the callback function once the download has started
* - browser sends download request and adds parameter with a token
* - server notices this token and adds a set cookie to the download response
* - browser now adds this cookie for the domain
* - JS periodically checks for this cookie and then knows when the download has started to call the callback
*
* @param {string} url download URL
* @param {function} callback function to call once the download has started
*/
handleDownload: function(url, callback) {
var randomToken = Math.random().toString(36).substring(2),
checkForDownloadCookie = function() {
if (!OC.Util.isCookieSetToValue('ocDownloadStarted', randomToken)){
return false;
} else {
callback();
return true;
}
};
if (url.indexOf('?') >= 0) {
url += '&';
} else {
url += '?';
}
OC.redirect(url + 'downloadStartSecret=' + randomToken);
OC.Util.waitFor(checkForDownloadCookie, 500);
}
};
Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250);
Files._updateStorageQuotasThrottled = _.throttle(Files._updateStorageQuotas, 30000);
OCA.Files.Files = Files;
})();
// TODO: move to FileList
var createDragShadow = function(event) {
// FIXME: inject file list instance somehow
/* global FileList, Files */
//select dragged file
var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked');
if (!isDragSelected) {
//select dragged file
FileList._selectFileEl($(event.target).parents('tr:first'), true, false);
}
// do not show drag shadow for too many files
var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize());
selectedFiles = _.sortBy(selectedFiles, FileList._fileInfoCompare);
if (!isDragSelected && selectedFiles.length === 1) {
//revert the selection
FileList._selectFileEl($(event.target).parents('tr:first'), false, false);
}
// build dragshadow
var dragshadow = $('<table class="dragshadow"></table>');
var tbody = $('<tbody></tbody>');
dragshadow.append(tbody);
var dir = FileList.getCurrentDirectory();
$(selectedFiles).each(function(i,elem) {
// TODO: refactor this with the table row creation code
var newtr = $('<tr/>')
.attr('data-dir', dir)
.attr('data-file', elem.name)
.attr('data-origin', elem.origin);
newtr.append($('<td class="filename" />').text(elem.name).css('background-size', 32));
newtr.append($('<td class="size" />').text(OC.Util.humanFileSize(elem.size)));
tbody.append(newtr);
if (elem.type === 'dir') {
newtr.find('td.filename')
.css('background-image', 'url(' + OC.MimeType.getIconUrl('folder') + ')');
} else {
var path = dir + '/' + elem.name;
Files.lazyLoadPreview(path, elem.mimetype, function(previewpath) {
newtr.find('td.filename')
.css('background-image', 'url(' + previewpath + ')');
}, null, null, elem.etag);
}
});
return dragshadow;
};
//options for file drag/drop
//start&stop handlers needs some cleaning up
// TODO: move to FileList class
var dragOptions={
revert: 'invalid',
revertDuration: 300,
opacity: 0.7,
appendTo: 'body',
cursorAt: { left: 24, top: 18 },
helper: createDragShadow,
cursor: 'move',
start: function(event, ui){
var $selectedFiles = $('td.filename input:checkbox:checked');
if (!$selectedFiles.length) {
$selectedFiles = $(this);
}
$selectedFiles.closest('tr').addClass('animate-opacity dragging');
$selectedFiles.closest('tr').filter('.ui-droppable').droppable( 'disable' );
// Show breadcrumbs menu
$('.crumbmenu').addClass('canDropChildren');
},
stop: function(event, ui) {
var $selectedFiles = $('td.filename input:checkbox:checked');
if (!$selectedFiles.length) {
$selectedFiles = $(this);
}
var $tr = $selectedFiles.closest('tr');
$tr.removeClass('dragging');
$tr.filter('.ui-droppable').droppable( 'enable' );
setTimeout(function() {
$tr.removeClass('animate-opacity');
}, 300);
// Hide breadcrumbs menu
$('.crumbmenu').removeClass('canDropChildren');
},
drag: function(event, ui) {
var scrollingArea = window;
var currentScrollTop = $(scrollingArea).scrollTop();
var scrollArea = Math.min(Math.floor($(window).innerHeight() / 2), 100);
var bottom = $(window).innerHeight() - scrollArea;
var top = $(window).scrollTop() + scrollArea;
if (event.pageY < top) {
$(scrollingArea).animate({
scrollTop: currentScrollTop - 10
}, 400);
} else if (event.pageY > bottom) {
$(scrollingArea).animate({
scrollTop: currentScrollTop + 10
}, 400);
}
}
};
// sane browsers support using the distance option
if ( $('html.ie').length === 0) {
dragOptions['distance'] = 20;
}
// TODO: move to FileList class
var folderDropOptions = {
hoverClass: "canDrop",
drop: function( event, ui ) {
// don't allow moving a file into a selected folder
/* global FileList */
if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) {
return false;
}
var $tr = $(this).closest('tr');
if (($tr.data('permissions') & OC.PERMISSION_CREATE) === 0) {
FileList._showPermissionDeniedNotification();
return false;
}
var targetPath = FileList.getCurrentDirectory() + '/' + $tr.data('file');
var files = FileList.getSelectedFiles();
if (files.length === 0) {
// single one selected without checkbox?
files = _.map(ui.helper.find('tr'), function(el) {
return FileList.elementToFile($(el));
});
}
FileList.move(_.pluck(files, 'name'), targetPath);
},
tolerance: 'pointer'
};
// for backward compatibility
window.Files = OCA.Files.Files;
/**
* Copyright (c) 2012 Erik Sargent <esthepiking at gmail dot com>
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
/*****************************
* Keyboard shortcuts for Files app
* ctrl/cmd+n: new folder
* ctrl/cmd+shift+n: new file
* esc (while new file context menu is open): close menu
* up/down: select file/folder
* enter: open file/folder
* delete/backspace: delete file/folder
*****************************/
(function(Files) {
var keys = [];
var keyCodes = {
shift: 16,
n: 78,
cmdFirefox: 224,
cmdOpera: 17,
leftCmdWebKit: 91,
rightCmdWebKit: 93,
ctrl: 17,
esc: 27,
downArrow: 40,
upArrow: 38,
enter: 13,
del: 46
};
function removeA(arr) {
var what, a = arguments,
L = a.length,
ax;
while (L > 1 && arr.length) {
what = a[--L];
while ((ax = arr.indexOf(what)) !== -1) {
arr.splice(ax, 1);
}
}
return arr;
}
function newFile() {
$("#new").addClass("active");
$(".popup.popupTop").toggle(true);
$('#new li[data-type="file"]').trigger('click');
removeA(keys, keyCodes.n);
}
function newFolder() {
$("#new").addClass("active");
$(".popup.popupTop").toggle(true);
$('#new li[data-type="folder"]').trigger('click');
removeA(keys, keyCodes.n);
}
function esc() {
$("#controls").trigger('click');
}
function down() {
var select = -1;
$("#fileList tr").each(function(index) {
if ($(this).hasClass("mouseOver")) {
select = index + 1;
$(this).removeClass("mouseOver");
}
});
if (select === -1) {
$("#fileList tr:first").addClass("mouseOver");
} else {
$("#fileList tr").each(function(index) {
if (index === select) {
$(this).addClass("mouseOver");
}
});
}
}
function up() {
var select = -1;
$("#fileList tr").each(function(index) {
if ($(this).hasClass("mouseOver")) {
select = index - 1;
$(this).removeClass("mouseOver");
}
});
if (select === -1) {
$("#fileList tr:last").addClass("mouseOver");
} else {
$("#fileList tr").each(function(index) {
if (index === select) {
$(this).addClass("mouseOver");
}
});
}
}
function enter() {
$("#fileList tr").each(function(index) {
if ($(this).hasClass("mouseOver")) {
$(this).removeClass("mouseOver");
$(this).find("span.nametext").trigger('click');
}
});
}
function del() {
$("#fileList tr").each(function(index) {
if ($(this).hasClass("mouseOver")) {
$(this).removeClass("mouseOver");
$(this).find("a.action.delete").trigger('click');
}
});
}
function rename() {
$("#fileList tr").each(function(index) {
if ($(this).hasClass("mouseOver")) {
$(this).removeClass("mouseOver");
$(this).find("a[data-action='Rename']").trigger('click');
}
});
}
Files.bindKeyboardShortcuts = function(document, $) {
$(document).keydown(function(event) { //check for modifier keys
if(!$(event.target).is('body')) {
return;
}
var preventDefault = false;
if ($.inArray(event.keyCode, keys) === -1) {
keys.push(event.keyCode);
}
if (
$.inArray(keyCodes.n, keys) !== -1 && ($.inArray(keyCodes.cmdFirefox, keys) !== -1 || $.inArray(keyCodes.cmdOpera, keys) !== -1 || $.inArray(keyCodes.leftCmdWebKit, keys) !== -1 || $.inArray(keyCodes.rightCmdWebKit, keys) !== -1 || $.inArray(keyCodes.ctrl, keys) !== -1 || event.ctrlKey)) {
preventDefault = true; //new file/folder prevent browser from responding
}
if (preventDefault) {
event.preventDefault(); //Prevent web browser from responding
event.stopPropagation();
return false;
}
});
$(document).keyup(function(event) {
// do your event.keyCode checks in here
if (
$.inArray(keyCodes.n, keys) !== -1 && ($.inArray(keyCodes.cmdFirefox, keys) !== -1 || $.inArray(keyCodes.cmdOpera, keys) !== -1 || $.inArray(keyCodes.leftCmdWebKit, keys) !== -1 || $.inArray(keyCodes.rightCmdWebKit, keys) !== -1 || $.inArray(keyCodes.ctrl, keys) !== -1 || event.ctrlKey)) {
if ($.inArray(keyCodes.shift, keys) !== -1) { //16=shift, New File
newFile();
} else { //New Folder
newFolder();
}
} else if ($("#new").hasClass("active") && $.inArray(keyCodes.esc, keys) !== -1) { //close new window
esc();
} else if ($.inArray(keyCodes.downArrow, keys) !== -1) { //select file
down();
} else if ($.inArray(keyCodes.upArrow, keys) !== -1) { //select file
up();
} else if (!$("#new").hasClass("active") && $.inArray(keyCodes.enter, keys) !== -1) { //open file
enter();
} else if (!$("#new").hasClass("active") && $.inArray(keyCodes.del, keys) !== -1) { //delete file
del();
}
removeA(keys, event.keyCode);
});
};
})((OCA.Files && OCA.Files.Files) || {});
/*
* @Copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* @author Vincent Petry
* @author Felix Nüsse <felix.nuesse@t-online.de>
*
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function () {
/**
* @class OCA.Files.Navigation
* @classdesc Navigation control for the files app sidebar.
*
* @param $el element containing the navigation
*/
var Navigation = function ($el) {
this.initialize($el);
};
/**
* @memberof OCA.Files
*/
Navigation.prototype = {
/**
* Currently selected item in the list
*/
_activeItem: null,
/**
* Currently selected container
*/
$currentContent: null,
/**
* Key for the quick-acces-list
*/
$quickAccessListKey: 'sublist-favorites',
/**
* Initializes the navigation from the given container
*
* @private
* @param $el element containing the navigation
*/
initialize: function ($el) {
this.$el = $el;
this._activeItem = null;
this.$currentContent = null;
this._setupEvents();
this.setInitialQuickaccessSettings();
},
/**
* Setup UI events
*/
_setupEvents: function () {
this.$el.on('click', 'li a', _.bind(this._onClickItem, this));
this.$el.on('click', 'li button', _.bind(this._onClickMenuButton, this));
var trashBinElement = $('.nav-trashbin');
trashBinElement.droppable({
over: function (event, ui) {
trashBinElement.addClass('dropzone-background');
},
out: function (event, ui) {
trashBinElement.removeClass('dropzone-background');
},
activate: function (event, ui) {
var element = trashBinElement.find('a').first();
element.addClass('nav-icon-trashbin-starred').removeClass('nav-icon-trashbin');
},
deactivate: function (event, ui) {
var element = trashBinElement.find('a').first();
element.addClass('nav-icon-trashbin').removeClass('nav-icon-trashbin-starred');
},
drop: function (event, ui) {
trashBinElement.removeClass('dropzone-background');
var $selectedFiles = $(ui.draggable);
// FIXME: when there are a lot of selected files the helper
// contains only a subset of them; the list of selected
// files should be gotten from the file list instead to
// ensure that all of them are removed.
var item = ui.helper.find('tr');
for (var i = 0; i < item.length; i++) {
$selectedFiles.trigger('droppedOnTrash', item[i].getAttribute('data-file'), item[i].getAttribute('data-dir'));
}
}
});
},
/**
* Returns the container of the currently active app.
*
* @return app container
*/
getActiveContainer: function () {
return this.$currentContent;
},
/**
* Returns the currently active item
*
* @return item ID
*/
getActiveItem: function () {
return this._activeItem;
},
/**
* Switch the currently selected item, mark it as selected and
* make the content container visible, if any.
*
* @param string itemId id of the navigation item to select
* @param array options "silent" to not trigger event
*/
setActiveItem: function (itemId, options) {
var currentItem = this.$el.find('li[data-id="' + itemId + '"]');
var itemDir = currentItem.data('dir');
var itemView = currentItem.data('view');
var oldItemId = this._activeItem;
if (itemId === this._activeItem) {
if (!options || !options.silent) {
this.$el.trigger(
new $.Event('itemChanged', {
itemId: itemId,
previousItemId: oldItemId,
dir: itemDir,
view: itemView
})
);
}
return;
}
this.$el.find('li a').removeClass('active');
if (this.$currentContent) {
this.$currentContent.addClass('hidden');
this.$currentContent.trigger(jQuery.Event('hide'));
}
this._activeItem = itemId;
currentItem.children('a').addClass('active');
this.$currentContent = $('#app-content-' + (typeof itemView === 'string' && itemView !== '' ? itemView : itemId));
this.$currentContent.removeClass('hidden');
if (!options || !options.silent) {
this.$currentContent.trigger(jQuery.Event('show', {
itemId: itemId,
previousItemId: oldItemId,
dir: itemDir,
view: itemView
}));
this.$el.trigger(
new $.Event('itemChanged', {
itemId: itemId,
previousItemId: oldItemId,
dir: itemDir,
view: itemView
})
);
}
},
/**
* Returns whether a given item exists
*/
itemExists: function (itemId) {
return this.$el.find('li[data-id="' + itemId + '"]').length;
},
/**
* Event handler for when clicking on an item.
*/
_onClickItem: function (ev) {
var $target = $(ev.target);
var itemId = $target.closest('li').attr('data-id');
if (!_.isUndefined(itemId)) {
this.setActiveItem(itemId);
}
ev.preventDefault();
},
/**
* Event handler for clicking a button
*/
_onClickMenuButton: function (ev) {
var $target = $(ev.target);
var $menu = $target.parent('li');
var itemId = $target.closest('button').attr('id');
var collapsibleToggles = [];
var dotmenuToggles = [];
if ($menu.hasClass('collapsible') && $menu.data('expandedstate')) {
$menu.toggleClass('open');
var show = $menu.hasClass('open') ? 1 : 0;
var key = $menu.data('expandedstate');
$.post(OC.generateUrl("/apps/files/api/v1/toggleShowFolder/" + key), {show: show});
}
dotmenuToggles.forEach(function foundToggle (item) {
if (item[0] === ("#" + itemId)) {
document.getElementById(item[1]).classList.toggle('open');
}
});
ev.preventDefault();
},
/**
* Sort initially as setup of sidebar for QuickAccess
*/
setInitialQuickaccessSettings: function () {
var quickAccessKey = this.$quickAccessListKey;
var quickAccessMenu = document.getElementById(quickAccessKey);
if (quickAccessMenu) {
var list = quickAccessMenu.getElementsByTagName('li');
this.QuickSort(list, 0, list.length - 1);
}
var favoritesListElement = $(quickAccessMenu).parent();
favoritesListElement.droppable({
over: function (event, ui) {
favoritesListElement.addClass('dropzone-background');
},
out: function (event, ui) {
favoritesListElement.removeClass('dropzone-background');
},
activate: function (event, ui) {
var element = favoritesListElement.find('a').first();
element.addClass('nav-icon-favorites-starred').removeClass('nav-icon-favorites');
},
deactivate: function (event, ui) {
var element = favoritesListElement.find('a').first();
element.addClass('nav-icon-favorites').removeClass('nav-icon-favorites-starred');
},
drop: function (event, ui) {
favoritesListElement.removeClass('dropzone-background');
var $selectedFiles = $(ui.draggable);
if (ui.helper.find('tr').size() === 1) {
var $tr = $selectedFiles.closest('tr');
if ($tr.attr("data-favorite")) {
return;
}
$selectedFiles.trigger('droppedOnFavorites', $tr.attr('data-file'));
} else {
// FIXME: besides the issue described for dropping on
// the trash bin, for favoriting it is not possible to
// use the data from the helper; due to some bugs the
// tags are not always added to the selected files, and
// thus that data can not be accessed through the helper
// to prevent triggering the favorite action on an
// already favorited file (which would remove it from
// favorites).
OC.Notification.showTemporary(t('files', 'You can only favorite a single file or folder at a time'));
}
}
});
},
/**
* Sorting-Algorithm for QuickAccess
*/
QuickSort: function (list, start, end) {
var lastMatch;
if (list.length > 1) {
lastMatch = this.quicksort_helper(list, start, end);
if (start < lastMatch - 1) {
this.QuickSort(list, start, lastMatch - 1);
}
if (lastMatch < end) {
this.QuickSort(list, lastMatch, end);
}
}
},
/**
* Sorting-Algorithm-Helper for QuickAccess
*/
quicksort_helper: function (list, start, end) {
var pivot = Math.floor((end + start) / 2);
var pivotElement = this.getCompareValue(list, pivot);
var i = start;
var j = end;
while (i <= j) {
while (this.getCompareValue(list, i) < pivotElement) {
i++;
}
while (this.getCompareValue(list, j) > pivotElement) {
j--;
}
if (i <= j) {
this.swap(list, i, j);
i++;
j--;
}
}
return i;
},
/**
* Sorting-Algorithm-Helper for QuickAccess
* This method allows easy access to the element which is sorted by.
*/
getCompareValue: function (nodes, int, strategy) {
return nodes[int].getElementsByTagName('a')[0].innerHTML.toLowerCase();
},
/**
* Sorting-Algorithm-Helper for QuickAccess
* This method allows easy swapping of elements.
*/
swap: function (list, j, i) {
var before = function(node, insertNode) {
node.parentNode.insertBefore(insertNode, node);
}
before(list[i], list[j]);
before(list[j], list[i]);
}
};
OCA.Files.Navigation = Navigation;
})();