diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index f8026cc..429e046 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -87,6 +87,11 @@ define([ that.load_list(); }); }); + + $('.rename-button').click($.proxy(this.rename_selected, this)); + $('.shutdown-button').click($.proxy(this.shutdown_selected, this)); + $('.duplicate-button').click($.proxy(this.duplicate_selected, this)); + $('.delete-button').click($.proxy(this.delete_selected, this)); } }; @@ -111,7 +116,7 @@ define([ // read non-notebook files as binary reader.readAsArrayBuffer(f); } - var item = that.new_item(0); + var item = that.new_item(0, true); item.addClass('new-file'); that.add_name_input(f.name, item, file_ext == '.ipynb' ? 'notebook' : 'file'); // Store the list item in the reader so we can use it later @@ -218,7 +223,7 @@ define([ var path = this.notebook_path; var offset = n_uploads; if (path !== '') { - item = this.new_item(offset); + item = this.new_item(offset, false); model = { type: 'directory', name: '..', @@ -229,34 +234,84 @@ define([ } for (var i=0; i').addClass("list_item").addClass("row"); - // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix'); - // item.css('border-top-style','none'); - item.append($("").addClass("col-md-12").append( - $('').addClass('item_icon') - ).append( - $("").addClass("item_link").append( - $("").addClass("item_name") - ) - ).append( - $('').addClass("item_buttons pull-right") - )); + /** + * Creates a new item. + * @param {integer} index + * @param {boolean} [selectable] - tristate, undefined: don't draw checkbox, + * false: don't draw checkbox but pad + * where it should be, true: draw checkbox. + * @return {JQuery} row + */ + NotebookList.prototype.new_item = function (index, selectable) { + var row = $('') + .addClass("list_item") + .addClass("row"); + + var item = $("") + .addClass("col-md-12") + .appendTo(row); + + var checkbox; + if (selectable !== undefined) { + checkbox = $('') + .attr('type', 'checkbox') + .appendTo(item); + } + + $('') + .addClass('item_icon') + .appendTo(item); + + var link = $("") + .addClass("item_link") + .appendTo(item); + + $("") + .addClass("item_name") + .appendTo(link); + + if (selectable === false) { + checkbox.css('visibility', 'hidden'); + } else if (selectable === true) { + var that = this; + link.click(function(e) { + e.stopPropagation(); + }); + checkbox.click(function(e) { + e.stopPropagation(); + that._selection_changed(); + }); + row.click(function(e) { + e.stopPropagation(); + checkbox.prop('checked', !checkbox.prop('checked')); + that._selection_changed(); + }); + } + + var buttons = $('') + .addClass("item_buttons pull-right") + .appendTo(item); + + $('') + .addClass('fa fa-power-off running-indicator') + .css('visible', 'hidden') + .appendTo(buttons); if (index === -1) { - this.element.append(item); + this.element.append(row); } else { - this.element.children().eq(index).after(item); + this.element.children().eq(index).after(row); } - return item; + return row; }; @@ -272,12 +327,58 @@ define([ file: 'edit', }; + NotebookList.prototype._selection_changed = function() { + var selected = []; + var has_notebook = false; + var has_directory = false; + $('.list_item :checked').each(function(index, item) { + var parent = $(item).parent().parent(); + selected.push({ + name: parent.data('name'), + path: parent.data('path'), + type: parent.data('type') + }); + + has_notebook = has_notebook || parent.data('type') == 'notebook'; + has_directory = has_directory || parent.data('type') == 'directory'; + }); + this.selected = selected; + + // Rename is only visible when one item is selected. + if (selected.length==1) { + $('.rename-button').css('display', 'inline-block'); + } else { + $('.rename-button').css('display', 'none'); + } + + // Shutdown is only visible when one or more notebooks are visible. + if (has_notebook) { + $('.shutdown-button').css('display', 'inline-block'); + } else { + $('.shutdown-button').css('display', 'none'); + } + + // Duplicate isn't visible if a directory is selected. + if (selected.length > 0 && !has_directory) { + $('.duplicate-button').css('display', 'inline-block'); + } else { + $('.duplicate-button').css('display', 'none'); + } + + // Delete is visible if one or more items are selected. + if (selected.length > 0) { + $('.delete-button').css('display', 'inline-block'); + } else { + $('.delete-button').css('display', 'none'); + } + }; NotebookList.prototype.add_link = function (model, item) { var path = model.path, name = model.name; item.data('name', name); item.data('path', path); + item.data('type', model.type); item.find(".item_name").text(name); var icon = NotebookList.icons[model.type]; var uri_prefix = NotebookList.uri_prefixes[model.type]; @@ -291,13 +392,8 @@ define([ ) ); - var can_duplicate = (model.type != 'directory'); - var can_rename = (model.type == 'directory'); - var can_delete = (model.type != 'notebook' || this.sessions[path] === undefined); - if (!can_delete) { - this.add_shutdown_button(item, this.sessions[path]); - } - this.add_actions_button(item, can_delete, can_duplicate, can_rename); + var running = (model.type == 'notebook' && this.sessions[path] !== undefined); + item.find(".item_buttons i.running-indicator").css('visibility', running ? '' : 'hidden'); // directory nav doesn't open new tabs // files, notebooks do @@ -329,188 +425,156 @@ define([ }; - NotebookList.prototype.add_shutdown_button = function (item, session) { + NotebookList.prototype.shutdown_selected = function() { var that = this; - var shutdown_button = $("").text("Shutdown").addClass("btn btn-xs btn-warning"). - click(function (e) { - var settings = { - processData : false, - cache : false, - type : "DELETE", - dataType : "json", - success : function () { - that.load_sessions(); - }, - error : utils.log_ajax_error, - }; - var url = utils.url_join_encode( - that.base_url, - 'api/sessions', - session - ); - $.ajax(url, settings); - return false; - }); - item.find(".item_buttons").append(shutdown_button); + var settings = { + processData : false, + cache : false, + type : "DELETE", + dataType : "json", + success : function () { + that.load_sessions(); + }, + error : utils.log_ajax_error, + }; + + this.selected.forEach(function(item) { + if (item.type == 'notebook') { + var session = that.sessions[item.path]; + if (session) { + var url = utils.url_join_encode( + that.base_url, + 'api/sessions', + session + ); + $.ajax(url, settings); + } + } + }); }; - NotebookList.prototype.add_actions_button = function (item, can_delete, can_duplicate, can_rename) { - var group = $("") - .addClass('btn-group') - .css('float', 'none') - .appendTo(item.find(".item_buttons")); - - var actions_button = $("") - .html("Actions ") - .addClass("btn btn-default btn-xs dropdown-toggle") - .attr('data-toggle', 'dropdown') - .attr('aria-expanded', 'false') - .appendTo(group); - - var actions_list = $('') - .addClass('dropdown-menu') - .attr('role', 'menu') - .appendTo(group); - - var create_action = function(label, callback) { - var item = $('') - .click(callback) - .appendTo(actions_list); - - var link = $('') - .attr('href', '#') - .html(label) - .appendTo(item); - }; + NotebookList.prototype.rename_selected = function() { + if (this.selected.length != 1) return; - if (can_delete) create_action('Delete', this.make_delete_callback(item)); - if (can_duplicate) create_action('Duplicate', this.make_duplicate_callback(item)); - if (can_rename) create_action('Rename', this.make_rename_callback(item)); + var that = this; + var path = this.selected[0].path; + var input = $('').attr('type','text').attr('size','25').addClass('form-control') + .val(path); + var dialog_body = $('').append( + $("").addClass("rename-message") + .text('Enter a new directory name:') + ).append( + $("") + ).append(input); + var d = dialog.modal({ + title : "Rename directory", + body : dialog_body, + buttons : { + OK : { + class: "btn-primary", + click: function() { + that.contents.rename(path, input.val()).then(function() { + that.load_list(); + }).catch(function(e) { + dialog.modal({ + title : "Error", + body : $('') + .text("An error occurred while renaming \"" + path + "\" to \"" + input.val() + "\".") + .append($('').addClass('alert alert-danger').text(String(e))), + buttons : { + OK : {} + } + }); + }); + } + }, + Cancel : {} + }, + open : function () { + // Upon ENTER, click the OK button. + input.keydown(function (event) { + if (event.which === keyboard.keycodes.enter) { + d.find('.btn-primary').first().click(); + return false; + } + }); + input.focus().select(); + } + }); }; - NotebookList.prototype.make_rename_callback = function (item) { - var notebooklist = this; - return function (e) { - // $(this) is the button that was clicked. - var that = $(this); - // We use the filename from the parent list_item element's - // data because the outer scope's values change as we iterate through the loop. - var parent_item = that.parents('div.list_item'); - var name = parent_item.data('name'); - var path = parent_item.data('path'); - var input = $('').attr('type','text').attr('size','25').addClass('form-control') - .val(path); - var dialog_body = $('').append( - $("").addClass("rename-message") - .text('Enter a new directory name:') - ).append( - $("") - ).append(input); - var d = dialog.modal({ - title : "Rename directory", - body : dialog_body, - buttons : { - OK : { - class: "btn-primary", - click: function() { - notebooklist.contents.rename(path, input.val()).then(function() { - notebooklist.load_list(); + NotebookList.prototype.delete_selected = function() { + var message; + if (this.selected.length == 1) { + message = 'Are you sure you want to permanently delete: ' + this.selected[0].name + '?'; + } else { + message = 'Are you sure you want to permanently delete the ' + this.selected.length + ' files selected?'; + } + var that = this; + dialog.modal({ + title : "Delete", + body : message, + buttons : { + Delete : { + class: "btn-danger", + click: function() { + that.selected.forEach(function(item) { + that.contents.delete(item.path).then(function() { + that.notebook_deleted(item.path); }).catch(function(e) { dialog.modal({ title : "Error", body : $('') - .text("An error occurred while renaming \"" + path + "\" to \"" + input.val() + "\".") + .text("An error occurred while deleting \"" + item.path + "\".") .append($('').addClass('alert alert-danger').text(String(e))), buttons : { OK : {} } }); }); - } - }, - Cancel : {} + }); + } }, - open : function () { - // Upon ENTER, click the OK button. - input.keydown(function (event) { - if (event.which === keyboard.keycodes.enter) { - d.find('.btn-primary').first().click(); - return false; - } - }); - input.focus().select(); - } - }); - return false; - }; + Cancel : {} + } + }); }; - NotebookList.prototype.make_delete_callback = function (item) { - var notebooklist = this; - return function (e) { - // $(this) is the button that was clicked. - var that = $(this); - // We use the filename from the parent list_item element's - // data because the outer scope's values change as we iterate through the loop. - var parent_item = that.parents('div.list_item'); - var name = parent_item.data('name'); - var path = parent_item.data('path'); - var message = 'Are you sure you want to permanently delete: ' + name + '?'; - dialog.modal({ - title : "Delete", - body : message, - buttons : { - Delete : { - class: "btn-danger", - click: function() { - notebooklist.contents.delete(path).then(function() { - notebooklist.notebook_deleted(path); + NotebookList.prototype.duplicate_selected = function() { + var message; + if (this.selected.length == 1) { + message = 'Are you sure you want to duplicate: ' + this.selected[0].name + '?'; + } else { + message = 'Are you sure you want to duplicate the ' + this.selected.length + ' files selected?'; + } + var that = this; + dialog.modal({ + title : "Delete", + body : message, + buttons : { + Duplicate : { + class: "btn-primary", + click: function() { + that.selected.forEach(function(item) { + that.contents.copy(item.path, that.notebook_path).then(function () { + that.load_list(); }).catch(function(e) { dialog.modal({ title : "Error", body : $('') - .text("An error occurred while deleting \"" + path + "\".") + .text("An error occurred while copying \"" + item.path + "\".") .append($('').addClass('alert alert-danger').text(String(e))), buttons : { OK : {} } }); }); - } - }, - Cancel : {} - } - }); - return false; - }; - }; - - NotebookList.prototype.make_duplicate_callback = function (item) { - var notebooklist = this; - return function (e) { - // $(this) is the button that was clicked. - var that = $(this); - var name = item.data('name'); - var path = item.data('path'); - var message = 'Are you sure you want to duplicate ' + name + '?'; - var copy_from = {copy_from : path}; - IPython.dialog.modal({ - title : "Duplicate " + name, - body : message, - buttons : { - Duplicate : { - class: "btn-primary", - click: function() { - notebooklist.contents.copy(path, notebooklist.notebook_path).then(function () { - notebooklist.load_list(); - }); - } - }, - Cancel : {} - } - }); - return false; - } + }); + } + }, + Cancel : {} + } + }); }; NotebookList.prototype.notebook_deleted = function(path) { @@ -522,6 +586,7 @@ define([ if (element.data("path") == path) { element.remove(); events.trigger('notebook_deleted.NotebookList'); + this._selection_changed(); } }); }; diff --git a/IPython/html/static/tree/less/tree.less b/IPython/html/static/tree/less/tree.less index 8ae733a..4f808ed 100644 --- a/IPython/html/static/tree/less/tree.less +++ b/IPython/html/static/tree/less/tree.less @@ -45,6 +45,10 @@ ul.breadcrumb { } } +.dynamic-buttons { + display: inline-block; +} + .list_toolbar [class*="span"] { min-height: @btn_small_height; } @@ -88,6 +92,10 @@ ul.breadcrumb { padding-left: @dashboard_lr_pad; padding-right: @dashboard_lr_pad; line-height: @btn_mini_height; + + input { + margin-right: @dashboard_lr_pad; + } } .item_name { @@ -102,11 +110,15 @@ ul.breadcrumb { } .item_buttons { + padding-top: @dashboard_tb_pad; line-height: 1em; + .btn-toolbar(); .btn { min-width: 13ex; } - .btn-toolbar(); + .running-indicator { + color: @brand-success; + } } .toolbar_info { @@ -225,3 +237,19 @@ ul#new-menu { } } } + +.delete-button { + display: none; +} + +.duplicate-button { + display: none; +} + +.rename-button { + display: none; +} + +.shutdown-button { + display: none; +} diff --git a/IPython/html/templates/tree.html b/IPython/html/templates/tree.html index 93dbab2..03ab471 100644 --- a/IPython/html/templates/tree.html +++ b/IPython/html/templates/tree.html @@ -37,6 +37,12 @@ data-terminals-available="{{terminals_available}}" + + Duplicate + Rename + Shutdown + Delete + New