diff options
Diffstat (limited to 'apps/files/js')
-rw-r--r-- | apps/files/js/file-upload.js | 15 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 352 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 47 |
3 files changed, 285 insertions, 129 deletions
diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index ab450dc5cac..9fe623075bc 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -175,7 +175,14 @@ OC.Upload = { * @param {function} callbacks.onCancel */ checkExistingFiles: function (selection, callbacks) { - // TODO check filelist before uploading and show dialog on conflicts, use callbacks + /* + $.each(selection.uploads, function(i, upload) { + var $row = OCA.Files.App.fileList.findFileEl(upload.files[0].name); + if ($row) { + // TODO check filelist before uploading and show dialog on conflicts, use callbacks + } + }); + */ callbacks.onNoConflicts(selection); }, @@ -417,11 +424,15 @@ OC.Upload = { data.textStatus = 'servererror'; data.errorThrown = t('files', 'Could not get result from server.'); fu._trigger('fail', e, data); + } else if (result[0].status === 'readonly') { + var original = result[0]; + var replacement = data.files[0]; + OC.dialogs.fileexists(data, original, replacement, OC.Upload); } else if (result[0].status === 'existserror') { //show "file already exists" dialog var original = result[0]; var replacement = data.files[0]; - OC.dialogs.fileexists(data, original, replacement, OC.Upload, fu); + OC.dialogs.fileexists(data, original, replacement, OC.Upload); } else if (result[0].status !== 'success') { //delete data.jqXHR; data.textStatus = 'servererror'; diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 5bf1618b0b8..da48cf29be0 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -8,7 +8,6 @@ * */ -/* global trashBinApp */ (function() { /** @@ -109,33 +108,37 @@ permissions: permissions, icon: icon, actionHandler: action, - displayName: displayName + displayName: displayName || name }); }, /** * Register action * - * @param {Object} action action object - * @param {String} action.name identifier of the action - * @param {String} action.displayName display name of the action, defaults - * to the name given in action.name - * @param {String} action.mime mime type - * @param {int} action.permissions permissions - * @param {(Function|String)} action.icon icon path to the icon or function - * that returns it - * @param {OCA.Files.FileActions~actionHandler} action.actionHandler action handler function + * @param {OCA.Files.FileAction} action object */ registerAction: function (action) { var mime = action.mime; var name = action.name; + var actionSpec = { + action: action.actionHandler, + name: name, + displayName: action.displayName, + mime: mime, + icon: action.icon, + permissions: action.permissions + }; + if (_.isUndefined(action.displayName)) { + actionSpec.displayName = t('files', name); + } + if (_.isFunction(action.render)) { + actionSpec.render = action.render; + } else { + actionSpec.render = _.bind(this._defaultRenderAction, this); + } if (!this.actions[mime]) { this.actions[mime] = {}; } - this.actions[mime][name] = { - action: action.actionHandler, - permissions: action.permissions, - displayName: action.displayName || t('files', name) - }; + this.actions[mime][name] = actionSpec; this.icons[name] = action.icon; this._notifyUpdateListeners('registerAction', {action: action}); }, @@ -213,6 +216,128 @@ 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) { + var name = actionSpec.name; + if (name === 'Download' || !isDefault) { + var $actionLink = this._makeActionLink(actionSpec, context); + context.$file.find('a.name>span.fileactions').append($actionLink); + return $actionLink; + } + }, + /** + * Renders the action link element + * + * @param {OCA.Files.FileAction} actionSpec action object + * @param {OCA.Files.FileActionContext} context action context + */ + _makeActionLink: function(actionSpec, context) { + var img = actionSpec.icon; + if (img && img.call) { + img = img(context.$file.attr('data-file')); + } + var html = '<a href="#">'; + if (img) { + html += '<img class="svg" alt="" src="' + img + '" />'; + } + if (actionSpec.displayName) { + html += '<span> ' + actionSpec.displayName + '</span></a>'; + } + + return $(html); + }, + /** + * Custom renderer for the "Rename" action. + * Displays the rename action as an icon behind the file name. + * + * @param {OCA.Files.FileAction} actionSpec file action to render + * @param {boolean} isDefault true if the action is a default action, + * false otherwise + * @param {OCAFiles.FileActionContext} context rendering context + */ + _renderRenameAction: function(actionSpec, isDefault, context) { + var $actionEl = this._makeActionLink(actionSpec, context); + var $container = context.$file.find('a.name span.nametext'); + $actionEl.find('img').attr('alt', t('files', 'Rename')); + $container.find('.action-rename').remove(); + $container.append($actionEl); + return $actionEl; + }, + /** + * Custom renderer for the "Delete" action. + * Displays the "Delete" action as a trash icon at the end of + * the table row. + * + * @param {OCA.Files.FileAction} actionSpec file action to render + * @param {boolean} isDefault true if the action is a default action, + * false otherwise + * @param {OCAFiles.FileActionContext} context rendering context + */ + _renderDeleteAction: function(actionSpec, isDefault, context) { + var mountType = context.$file.attr('data-mounttype'); + var deleteTitle = t('files', 'Delete'); + if (mountType === 'external-root') { + deleteTitle = t('files', 'Disconnect storage'); + } else if (mountType === 'shared-root') { + deleteTitle = t('files', 'Unshare'); + } + var $actionLink = $('<a href="#" original-title="' + + escapeHTML(deleteTitle) + + '" class="action delete icon-delete" />' + ); + var $container = context.$file.find('td:last'); + $container.find('.delete').remove(); + $container.append($actionLink); + return $actionLink; + }, + /** + * 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 {OCAFiles.FileActionContext} context rendering context + */ + _renderAction: function(actionSpec, isDefault, context) { + var $actionEl = actionSpec.render(actionSpec, isDefault, context); + if (!$actionEl || !$actionEl.length) { + return; + } + $actionEl.addClass('action action-' + actionSpec.name.toLowerCase()); + $actionEl.attr('data-action', actionSpec.name); + $actionEl.on( + 'click', { + a: null + }, + function(event) { + var $file = $(event.target).closest('tr'); + var currentFile = $file.find('td.filename'); + var fileName = $file.attr('data-file'); + event.stopPropagation(); + event.preventDefault(); + + context.fileActions.currentFile = currentFile; + // also set on global object for legacy apps + window.FileActions.currentFile = currentFile; + + actionSpec.action( + fileName, + _.extend(context, { + dir: $file.attr('data-path') || context.fileList.getCurrentDirectory() + }) + ); + } + ); + return $actionEl; + }, + /** * 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 @@ -226,107 +351,51 @@ return; } this.currentFile = parent; - var $tr = parent.closest('tr'); var self = this; - var actions = this.getActions(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions()); - var file = this.getCurrentFile(); + var $tr = parent.closest('tr'); + var actions = this.getActions( + this.getCurrentMimeType(), + this.getCurrentType(), + this.getCurrentPermissions() + ); var nameLinks; if ($tr.data('renaming')) { return; } - // recreate fileactions + // recreate fileactions container nameLinks = parent.children('a.name'); nameLinks.find('.fileactions, .nametext .action').remove(); nameLinks.append('<span class="fileactions" />'); - var defaultAction = this.getDefault(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions()); - - var actionHandler = function (event) { - event.stopPropagation(); - event.preventDefault(); + var defaultAction = this.getDefault( + this.getCurrentMimeType(), + this.getCurrentType(), + this.getCurrentPermissions() + ); - self.currentFile = event.data.elem; - // also set on global object for legacy apps - window.FileActions.currentFile = self.currentFile; - - var file = self.getCurrentFile(); - var $tr = $(this).closest('tr'); - - event.data.actionFunc(file, { - $file: $tr, - fileList: fileList, - fileActions: self, - dir: $tr.attr('data-path') || fileList.getCurrentDirectory() - }); - }; - - var addAction = function (name, action, displayName) { - - if ((name === 'Download' || action !== defaultAction) && name !== 'Delete') { - - var img = self.icons[name], - actionText = displayName, - actionContainer = 'a.name>span.fileactions'; - - if (name === 'Rename') { - // rename has only an icon which appears behind - // the file name - actionText = ''; - actionContainer = 'a.name span.nametext'; - } - if (img.call) { - img = img(file); - } - var html = '<a href="#" class="action action-' + name.toLowerCase() + '" data-action="' + name + '">'; - if (img) { - html += '<img class ="svg" alt="" src="' + img + '" />'; - } - html += '<span> ' + actionText + '</span></a>'; - - var element = $(html); - element.data('action', name); - element.on('click', {a: null, elem: parent, actionFunc: actions[name].action}, actionHandler); - parent.find(actionContainer).append(element); - } - - }; - - $.each(actions, function (name, action) { + $.each(actions, function (name, actionSpec) { if (name !== 'Share') { - displayName = action.displayName; - ah = action.action; - - addAction(name, ah, displayName); + self._renderAction( + actionSpec, + actionSpec.action === defaultAction, { + $file: $tr, + fileActions: this, + fileList : fileList + } + ); } }); - if(actions.Share){ - displayName = t('files', 'Share'); - addAction('Share', actions.Share, displayName); - } - - // remove the existing delete action - parent.parent().children().last().find('.action.delete').remove(); - if (actions['Delete']) { - var img = self.icons['Delete']; - var html; - var mountType = $tr.attr('data-mounttype'); - var deleteTitle = t('files', 'Delete'); - if (mountType === 'external-root') { - deleteTitle = t('files', 'Disconnect storage'); - } else if (mountType === 'shared-root') { - deleteTitle = t('files', 'Unshare'); - } else if (fileList.id === 'trashbin') { - deleteTitle = t('files', 'Delete permanently'); - } - - if (img.call) { - img = img(file); - } - html = '<a href="#" original-title="' + escapeHTML(deleteTitle) + '" class="action delete icon-delete" />'; - var element = $(html); - element.data('action', actions['Delete']); - element.on('click', {a: null, elem: parent, actionFunc: actions['Delete'].action}, actionHandler); - parent.parent().children().last().append(element); + // added here to make sure it's always the last action + var shareActionSpec = actions.Share; + if (shareActionSpec){ + this._renderAction( + shareActionSpec, + shareActionSpec.action === defaultAction, { + $file: $tr, + fileActions: this, + fileList: fileList + } + ); } if (triggerEvent){ @@ -350,18 +419,34 @@ * Register the actions that are used by default for the files app. */ registerDefaultActions: function() { - this.register('all', 'Delete', OC.PERMISSION_DELETE, function () { - return OC.imagePath('core', 'actions/delete'); - }, function (filename, context) { - context.fileList.do_delete(filename, context.dir); - $('.tipsy').remove(); + this.registerAction({ + name: 'Delete', + displayName: '', + mime: 'all', + permissions: OC.PERMISSION_DELETE, + icon: function() { + return OC.imagePath('core', 'actions/delete'); + }, + render: _.bind(this._renderDeleteAction, this), + actionHandler: function(fileName, context) { + context.fileList.do_delete(fileName, context.dir); + $('.tipsy').remove(); + } }); // t('files', 'Rename') - this.register('all', 'Rename', OC.PERMISSION_UPDATE, function () { - return OC.imagePath('core', 'actions/rename'); - }, function (filename, context) { - context.fileList.rename(filename); + this.registerAction({ + name: 'Rename', + displayName: '', + mime: 'all', + permissions: OC.PERMISSION_UPDATE, + icon: function() { + return OC.imagePath('core', 'actions/rename'); + }, + render: _.bind(this._renderRenameAction, this), + actionHandler: function (filename, context) { + context.fileList.rename(filename); + } }); this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { @@ -389,6 +474,47 @@ OCA.Files.FileActions = FileActions; /** + * 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} displayName display name of the action, 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 + * @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 + */ + + /** * Action handler function for file actions * * @callback OCA.Files.FileActions~actionHandler diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index f4b45fc58bf..b4702ce4f2b 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -94,6 +94,12 @@ fileActions: 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> */ @@ -166,6 +172,9 @@ } 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'); @@ -200,7 +209,7 @@ this.$el.on('show', this._onResize); this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); - this.$fileList.on('change', 'td.filename>input:checkbox', _.bind(this._onClickFileCheckbox, this)); + this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this)); this.$el.on('urlChanged', _.bind(this._onUrlChanged, this)); this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this)); this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this)); @@ -215,6 +224,8 @@ self.scrollTo(options.scrollTo); }); } + + OC.Plugins.attach('OCA.Files.FileList', this); }, /** @@ -224,6 +235,7 @@ // 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); }, /** @@ -275,7 +287,7 @@ * @param state true to select, false to deselect */ _selectFileEl: function($tr, state) { - var $checkbox = $tr.find('td.filename>input:checkbox'); + var $checkbox = $tr.find('td.filename>.selectCheckBox'); var oldData = !!this._selectedFiles[$tr.data('id')]; var data; $checkbox.prop('checked', state); @@ -324,7 +336,7 @@ else { this._lastChecked = $tr; } - var $checkbox = $tr.find('td.filename>input:checkbox'); + var $checkbox = $tr.find('td.filename>.selectCheckBox'); this._selectFileEl($tr, !$checkbox.prop('checked')); this.updateSelectionSummary(); } else { @@ -366,7 +378,7 @@ */ _onClickSelectAll: function(e) { var checked = $(e.target).prop('checked'); - this.$fileList.find('td.filename>input:checkbox').prop('checked', checked) + this.$fileList.find('td.filename>.selectCheckBox').prop('checked', checked) .closest('tr').toggleClass('selected', checked); this._selectedFiles = {}; this._selectionSummary.clear(); @@ -554,7 +566,7 @@ this.$fileList.append(tr); if (isAllSelected || this._selectedFiles[fileData.id]) { tr.addClass('selected'); - tr.find('input:checkbox').prop('checked', true); + tr.find('.selectCheckBox').prop('checked', true); } if (animate) { tr.addClass('appear transparent'); @@ -683,10 +695,8 @@ } // filename td - td = $('<td></td>').attr({ - "class": "filename", - "style": 'background-image:url(' + icon + '); background-size: 32px;' - }); + td = $('<td class="filename"></td>'); + // linkUrl if (type === 'dir') { @@ -695,8 +705,16 @@ else { linkUrl = this.getDownloadUrl(name, path); } - td.append('<input id="select-' + this.id + '-' + fileData.id + - '" type="checkbox" /><label for="select-' + this.id + '-' + fileData.id + '"></label>'); + if (this._allowSelection) { + td.append( + '<input id="select-' + this.id + '-' + fileData.id + + '" type="checkbox" class="selectCheckBox"/><label for="select-' + this.id + '-' + fileData.id + '">' + + '<div class="thumbnail" style="background-image:url(' + icon + '); background-size: 32px;"></div>' + + '</label>' + ); + } else { + td.append('<div class="thumbnail" style="background-image:url(' + icon + '); background-size: 32px;"></div>'); + } var linkElem = $('<a></a>').attr({ "class": "name", "href": linkUrl @@ -901,6 +919,7 @@ this.fileActions.display(filenameTd, !options.silent, this); if (fileData.isPreviewAvailable) { + var iconDiv = filenameTd.find('.thumbnail'); // lazy load / newly inserted td ? if (options.animate) { this.lazyLoadPreview({ @@ -908,7 +927,7 @@ mime: mime, etag: fileData.etag, callback: function(url) { - filenameTd.css('background-image', 'url(' + url + ')'); + iconDiv.css('background-image', 'url(' + url + ')'); } }); } @@ -920,7 +939,7 @@ }; var previewUrl = this.generatePreviewUrl(urlSpec); previewUrl = previewUrl.replace('(', '%28').replace(')', '%29'); - filenameTd.css('background-image', 'url(' + previewUrl + ')'); + iconDiv.css('background-image', 'url(' + previewUrl + ')'); } } return tr; @@ -1526,7 +1545,7 @@ var fileEl = self.remove(file, {updateSummary: false}); // FIXME: not sure why we need this after the // element isn't even in the DOM any more - fileEl.find('input[type="checkbox"]').prop('checked', false); + fileEl.find('.selectCheckBox').prop('checked', false); fileEl.removeClass('selected'); self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}); }); |