##// END OF EJS Templates
Add actions dropdown to tree view
Jonathan Frederic -
Show More
@@ -1,548 +1,640 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'base/js/events',
10 10 'base/js/keyboard',
11 11 ], function(IPython, $, utils, dialog, events, keyboard) {
12 12 "use strict";
13 13
14 14 var NotebookList = function (selector, options) {
15 15 /**
16 16 * Constructor
17 17 *
18 18 * Parameters:
19 19 * selector: string
20 20 * options: dictionary
21 21 * Dictionary of keyword arguments.
22 22 * session_list: SessionList instance
23 23 * element_name: string
24 24 * base_url: string
25 25 * notebook_path: string
26 26 * contents: Contents instance
27 27 */
28 28 var that = this;
29 29 this.session_list = options.session_list;
30 30 // allow code re-use by just changing element_name in kernellist.js
31 31 this.element_name = options.element_name || 'notebook';
32 32 this.selector = selector;
33 33 if (this.selector !== undefined) {
34 34 this.element = $(selector);
35 35 this.style();
36 36 this.bind_events();
37 37 }
38 38 this.notebooks_list = [];
39 39 this.sessions = {};
40 40 this.base_url = options.base_url || utils.get_body_data("baseUrl");
41 41 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
42 42 this.contents = options.contents;
43 43 if (this.session_list && this.session_list.events) {
44 44 this.session_list.events.on('sessions_loaded.Dashboard',
45 45 function(e, d) { that.sessions_loaded(d); });
46 46 }
47 47 };
48 48
49 49 NotebookList.prototype.style = function () {
50 50 var prefix = '#' + this.element_name;
51 51 $(prefix + '_toolbar').addClass('list_toolbar');
52 52 $(prefix + '_list_info').addClass('toolbar_info');
53 53 $(prefix + '_buttons').addClass('toolbar_buttons');
54 54 $(prefix + '_list_header').addClass('list_header');
55 55 this.element.addClass("list_container");
56 56 };
57 57
58 58 NotebookList.prototype.bind_events = function () {
59 59 var that = this;
60 60 $('#refresh_' + this.element_name + '_list').click(function () {
61 61 that.load_sessions();
62 62 });
63 63 this.element.bind('dragover', function () {
64 64 return false;
65 65 });
66 66 this.element.bind('drop', function(event){
67 67 that.handleFilesUpload(event,'drop');
68 68 return false;
69 69 });
70 70
71 71 // Bind events for singleton controls.
72 72 if (!NotebookList._bound_singletons) {
73 73 NotebookList._bound_singletons = true;
74 74 $('#new-file').click(function(e) {
75 75 var w = window.open();
76 76 that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'}).then(function(data) {
77 77 var url = utils.url_join_encode(
78 78 that.base_url, 'edit', data.path
79 79 );
80 80 w.location = url;
81 81 });
82 82 that.load_sessions();
83 83 });
84 84 $('#new-folder').click(function(e) {
85 85 that.contents.new_untitled(that.notebook_path || '', {type: 'directory'})
86 86 .then(function(){
87 87 that.load_list();
88 88 });
89 89 });
90 90 }
91 91 };
92 92
93 93 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
94 94 var that = this;
95 95 var files;
96 96 if(dropOrForm =='drop'){
97 97 files = event.originalEvent.dataTransfer.files;
98 98 } else
99 99 {
100 100 files = event.originalEvent.target.files;
101 101 }
102 102 for (var i = 0; i < files.length; i++) {
103 103 var f = files[i];
104 104 var name_and_ext = utils.splitext(f.name);
105 105 var file_ext = name_and_ext[1];
106 106
107 107 var reader = new FileReader();
108 108 if (file_ext === '.ipynb') {
109 109 reader.readAsText(f);
110 110 } else {
111 111 // read non-notebook files as binary
112 112 reader.readAsArrayBuffer(f);
113 113 }
114 114 var item = that.new_item(0);
115 115 item.addClass('new-file');
116 116 that.add_name_input(f.name, item, file_ext == '.ipynb' ? 'notebook' : 'file');
117 117 // Store the list item in the reader so we can use it later
118 118 // to know which item it belongs to.
119 119 $(reader).data('item', item);
120 120 reader.onload = function (event) {
121 121 var item = $(event.target).data('item');
122 122 that.add_file_data(event.target.result, item);
123 123 that.add_upload_button(item);
124 124 };
125 125 reader.onerror = function (event) {
126 126 var item = $(event.target).data('item');
127 127 var name = item.data('name');
128 128 item.remove();
129 129 dialog.modal({
130 130 title : 'Failed to read file',
131 131 body : "Failed to read file '" + name + "'",
132 132 buttons : {'OK' : { 'class' : 'btn-primary' }}
133 133 });
134 134 };
135 135 }
136 136 // Replace the file input form wth a clone of itself. This is required to
137 137 // reset the form. Otherwise, if you upload a file, delete it and try to
138 138 // upload it again, the changed event won't fire.
139 139 var form = $('input.fileinput');
140 140 form.replaceWith(form.clone(true));
141 141 return false;
142 142 };
143 143
144 144 NotebookList.prototype.clear_list = function (remove_uploads) {
145 145 /**
146 146 * Clears the navigation tree.
147 147 *
148 148 * Parameters
149 149 * remove_uploads: bool=False
150 150 * Should upload prompts also be removed from the tree.
151 151 */
152 152 if (remove_uploads) {
153 153 this.element.children('.list_item').remove();
154 154 } else {
155 155 this.element.children('.list_item:not(.new-file)').remove();
156 156 }
157 157 };
158 158
159 159 NotebookList.prototype.load_sessions = function(){
160 160 this.session_list.load_sessions();
161 161 };
162 162
163 163
164 164 NotebookList.prototype.sessions_loaded = function(data){
165 165 this.sessions = data;
166 166 this.load_list();
167 167 };
168 168
169 169 NotebookList.prototype.load_list = function () {
170 170 var that = this;
171 171 this.contents.list_contents(that.notebook_path).then(
172 172 $.proxy(this.draw_notebook_list, this),
173 173 function(error) {
174 174 that.draw_notebook_list({content: []}, "Server error: " + error.message);
175 175 }
176 176 );
177 177 };
178 178
179 179 /**
180 180 * Draw the list of notebooks
181 181 * @method draw_notebook_list
182 182 * @param {Array} list An array of dictionaries representing files or
183 183 * directories.
184 184 * @param {String} error_msg An error message
185 185 */
186 186
187 187
188 188 var type_order = {'directory':0,'notebook':1,'file':2};
189 189
190 190 NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
191 191 list.content.sort(function(a, b) {
192 192 if (type_order[a['type']] < type_order[b['type']]) {
193 193 return -1;
194 194 }
195 195 if (type_order[a['type']] > type_order[b['type']]) {
196 196 return 1;
197 197 }
198 198 if (a['name'] < b['name']) {
199 199 return -1;
200 200 }
201 201 if (a['name'] > b['name']) {
202 202 return 1;
203 203 }
204 204 return 0;
205 205 });
206 206 var message = error_msg || 'Notebook list empty.';
207 207 var item = null;
208 208 var model = null;
209 209 var len = list.content.length;
210 210 this.clear_list();
211 211 var n_uploads = this.element.children('.list_item').length;
212 212 if (len === 0) {
213 213 item = this.new_item(0);
214 214 var span12 = item.children().first();
215 215 span12.empty();
216 216 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
217 217 }
218 218 var path = this.notebook_path;
219 219 var offset = n_uploads;
220 220 if (path !== '') {
221 221 item = this.new_item(offset);
222 222 model = {
223 223 type: 'directory',
224 224 name: '..',
225 225 path: utils.url_path_split(path)[0],
226 226 };
227 227 this.add_link(model, item);
228 228 offset += 1;
229 229 }
230 230 for (var i=0; i<len; i++) {
231 231 model = list.content[i];
232 232 item = this.new_item(i+offset);
233 233 this.add_link(model, item);
234 234 }
235 235 // Trigger an event when we've finished drawing the notebook list.
236 236 events.trigger('draw_notebook_list.NotebookList');
237 237 };
238 238
239 239
240 240 NotebookList.prototype.new_item = function (index) {
241 241 var item = $('<div/>').addClass("list_item").addClass("row");
242 242 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
243 243 // item.css('border-top-style','none');
244 244 item.append($("<div/>").addClass("col-md-12").append(
245 245 $('<i/>').addClass('item_icon')
246 246 ).append(
247 247 $("<a/>").addClass("item_link").append(
248 248 $("<span/>").addClass("item_name")
249 249 )
250 250 ).append(
251 251 $('<div/>').addClass("item_buttons pull-right")
252 252 ));
253 253
254 254 if (index === -1) {
255 255 this.element.append(item);
256 256 } else {
257 257 this.element.children().eq(index).after(item);
258 258 }
259 259 return item;
260 260 };
261 261
262 262
263 263 NotebookList.icons = {
264 264 directory: 'folder_icon',
265 265 notebook: 'notebook_icon',
266 266 file: 'file_icon',
267 267 };
268 268
269 269 NotebookList.uri_prefixes = {
270 270 directory: 'tree',
271 271 notebook: 'notebooks',
272 272 file: 'edit',
273 273 };
274 274
275 275
276 276 NotebookList.prototype.add_link = function (model, item) {
277 277 var path = model.path,
278 278 name = model.name;
279 279 item.data('name', name);
280 280 item.data('path', path);
281 281 item.find(".item_name").text(name);
282 282 var icon = NotebookList.icons[model.type];
283 283 var uri_prefix = NotebookList.uri_prefixes[model.type];
284 284 item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
285 285 var link = item.find("a.item_link")
286 286 .attr('href',
287 287 utils.url_join_encode(
288 288 this.base_url,
289 289 uri_prefix,
290 290 path
291 291 )
292 292 );
293
294 var can_duplicate = (model.type != 'directory');
295 var can_rename = (model.type == 'directory');
296 var can_delete = (model.type != 'notebook' || this.sessions[path] === undefined);
297 if (!can_delete) {
298 this.add_shutdown_button(item, this.sessions[path]);
299 }
300 this.add_actions_button(item, can_delete, can_duplicate, can_rename);
301
293 302 // directory nav doesn't open new tabs
294 303 // files, notebooks do
295 304 if (model.type !== "directory") {
296 305 link.attr('target','_blank');
297 306 }
298 if (model.type !== 'directory') {
299 this.add_duplicate_button(item);
300 }
301 if (model.type == 'file') {
302 this.add_delete_button(item);
303 } else if (model.type == 'notebook') {
304 if (this.sessions[path] === undefined){
305 this.add_delete_button(item);
306 } else {
307 this.add_shutdown_button(item, this.sessions[path]);
308 }
309 }
310 307 };
311 308
312 309
313 310 NotebookList.prototype.add_name_input = function (name, item, icon_type) {
314 311 item.data('name', name);
315 312 item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
316 313 item.find(".item_name").empty().append(
317 314 $('<input/>')
318 315 .addClass("filename_input")
319 316 .attr('value', name)
320 317 .attr('size', '30')
321 318 .attr('type', 'text')
322 319 .keyup(function(event){
323 320 if(event.keyCode == 13){item.find('.upload_button').click();}
324 321 else if(event.keyCode == 27){item.remove();}
325 322 })
326 323 );
327 324 };
328 325
329 326
330 327 NotebookList.prototype.add_file_data = function (data, item) {
331 328 item.data('filedata', data);
332 329 };
333 330
334 331
335 332 NotebookList.prototype.add_shutdown_button = function (item, session) {
336 333 var that = this;
337 334 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-warning").
338 335 click(function (e) {
339 336 var settings = {
340 337 processData : false,
341 338 cache : false,
342 339 type : "DELETE",
343 340 dataType : "json",
344 341 success : function () {
345 342 that.load_sessions();
346 343 },
347 344 error : utils.log_ajax_error,
348 345 };
349 346 var url = utils.url_join_encode(
350 347 that.base_url,
351 348 'api/sessions',
352 349 session
353 350 );
354 351 $.ajax(url, settings);
355 352 return false;
356 353 });
357 354 item.find(".item_buttons").append(shutdown_button);
358 355 };
359 356
360 NotebookList.prototype.add_duplicate_button = function (item) {
357 NotebookList.prototype.add_actions_button = function (item, can_delete, can_duplicate, can_rename) {
358 var group = $("<div/>")
359 .addClass('btn-group')
360 .css('float', 'none')
361 .appendTo(item.find(".item_buttons"));
362
363 var actions_button = $("<button/>")
364 .html("Actions <span class='caret'></span>")
365 .addClass("btn btn-default btn-xs dropdown-toggle")
366 .attr('data-toggle', 'dropdown')
367 .attr('aria-expanded', 'false')
368 .appendTo(group);
369
370 var actions_list = $('<ul/>')
371 .addClass('dropdown-menu')
372 .attr('role', 'menu')
373 .appendTo(group);
374
375 var create_action = function(label, callback) {
376 var item = $('<li/>')
377 .click(callback)
378 .appendTo(actions_list);
379
380 var link = $('<a/>')
381 .attr('href', '#')
382 .html(label)
383 .appendTo(item);
384 };
385
386 if (can_delete) create_action('Delete', this.make_delete_callback(item));
387 if (can_duplicate) create_action('Duplicate', this.make_duplicate_callback(item));
388 if (can_rename) create_action('Rename', this.make_rename_callback(item));
389 };
390
391 NotebookList.prototype.make_rename_callback = function (item) {
361 392 var notebooklist = this;
362 var duplicate_button = $("<button/>").text("Duplicate").addClass("btn btn-default btn-xs").
363 click(function (e) {
364 // $(this) is the button that was clicked.
365 var that = $(this);
366 var name = item.data('name');
367 var path = item.data('path');
368 var message = 'Are you sure you want to duplicate ' + name + '?';
369 var copy_from = {copy_from : path};
370 IPython.dialog.modal({
371 title : "Duplicate " + name,
372 body : message,
373 buttons : {
374 Duplicate : {
375 class: "btn-primary",
376 click: function() {
377 notebooklist.contents.copy(path, notebooklist.notebook_path).then(function () {
378 notebooklist.load_list();
393 return function (e) {
394 // $(this) is the button that was clicked.
395 var that = $(this);
396 // We use the filename from the parent list_item element's
397 // data because the outer scope's values change as we iterate through the loop.
398 var parent_item = that.parents('div.list_item');
399 var name = parent_item.data('name');
400 var path = parent_item.data('path');
401 var input = $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
402 .val(path);
403 var dialog_body = $('<div/>').append(
404 $("<p/>").addClass("rename-message")
405 .text('Enter a new directory name:')
406 ).append(
407 $("<br/>")
408 ).append(input);
409 var d = dialog.modal({
410 title : "Rename directory",
411 body : dialog_body,
412 buttons : {
413 OK : {
414 class: "btn-primary",
415 click: function() {
416 notebooklist.contents.rename(path, input.val()).then(function() {
417 notebooklist.load_list();
418 }).catch(function(e) {
419 dialog.modal({
420 title : "Error",
421 body : $('<div/>')
422 .text("An error occurred while renaming \"" + path + "\" to \"" + input.val() + "\".")
423 .append($('<div/>').addClass('alert alert-danger').text(String(e))),
424 buttons : {
425 OK : {}
426 }
379 427 });
380 }
381 },
382 Cancel : {}
383 }
384 });
385 return false;
428 });
429 }
430 },
431 Cancel : {}
432 },
433 open : function () {
434 // Upon ENTER, click the OK button.
435 input.keydown(function (event) {
436 if (event.which === keyboard.keycodes.enter) {
437 d.find('.btn-primary').first().click();
438 return false;
439 }
440 });
441 input.focus().select();
442 }
386 443 });
387 item.find(".item_buttons").append(duplicate_button);
444 return false;
445 };
388 446 };
389 447
390 NotebookList.prototype.add_delete_button = function (item) {
448 NotebookList.prototype.make_delete_callback = function (item) {
391 449 var notebooklist = this;
392 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
393 click(function (e) {
394 // $(this) is the button that was clicked.
395 var that = $(this);
396 // We use the filename from the parent list_item element's
397 // data because the outer scope's values change as we iterate through the loop.
398 var parent_item = that.parents('div.list_item');
399 var name = parent_item.data('name');
400 var path = parent_item.data('path');
401 var message = 'Are you sure you want to permanently delete the file: ' + name + '?';
402 dialog.modal({
403 title : "Delete file",
404 body : message,
405 buttons : {
406 Delete : {
407 class: "btn-danger",
408 click: function() {
409 notebooklist.contents.delete(path).then(
410 function() {
411 notebooklist.notebook_deleted(path);
450 return function (e) {
451 // $(this) is the button that was clicked.
452 var that = $(this);
453 // We use the filename from the parent list_item element's
454 // data because the outer scope's values change as we iterate through the loop.
455 var parent_item = that.parents('div.list_item');
456 var name = parent_item.data('name');
457 var path = parent_item.data('path');
458 var message = 'Are you sure you want to permanently delete: ' + name + '?';
459 dialog.modal({
460 title : "Delete",
461 body : message,
462 buttons : {
463 Delete : {
464 class: "btn-danger",
465 click: function() {
466 notebooklist.contents.delete(path).then(function() {
467 notebooklist.notebook_deleted(path);
468 }).catch(function(e) {
469 dialog.modal({
470 title : "Error",
471 body : $('<div/>')
472 .text("An error occurred while deleting \"" + path + "\".")
473 .append($('<div/>').addClass('alert alert-danger').text(String(e))),
474 buttons : {
475 OK : {}
412 476 }
413 );
414 }
415 },
416 Cancel : {}
417 }
418 });
419 return false;
477 });
478 });
479 }
480 },
481 Cancel : {}
482 }
420 483 });
421 item.find(".item_buttons").append(delete_button);
484 return false;
485 };
486 };
487
488 NotebookList.prototype.make_duplicate_callback = function (item) {
489 var notebooklist = this;
490 return function (e) {
491 // $(this) is the button that was clicked.
492 var that = $(this);
493 var name = item.data('name');
494 var path = item.data('path');
495 var message = 'Are you sure you want to duplicate ' + name + '?';
496 var copy_from = {copy_from : path};
497 IPython.dialog.modal({
498 title : "Duplicate " + name,
499 body : message,
500 buttons : {
501 Duplicate : {
502 class: "btn-primary",
503 click: function() {
504 notebooklist.contents.copy(path, notebooklist.notebook_path).then(function () {
505 notebooklist.load_list();
506 });
507 }
508 },
509 Cancel : {}
510 }
511 });
512 return false;
513 }
422 514 };
423 515
424 516 NotebookList.prototype.notebook_deleted = function(path) {
425 517 /**
426 518 * Remove the deleted notebook.
427 519 */
428 520 $( ":data(path)" ).each(function() {
429 521 var element = $(this);
430 522 if (element.data("path") == path) {
431 523 element.remove();
432 524 events.trigger('notebook_deleted.NotebookList');
433 525 }
434 526 });
435 527 };
436 528
437 529
438 530 NotebookList.prototype.add_upload_button = function (item) {
439 531 var that = this;
440 532 var upload_button = $('<button/>').text("Upload")
441 533 .addClass('btn btn-primary btn-xs upload_button')
442 534 .click(function (e) {
443 535 var filename = item.find('.item_name > input').val();
444 536 var path = utils.url_path_join(that.notebook_path, filename);
445 537 var filedata = item.data('filedata');
446 538 var format = 'text';
447 539 if (filename.length === 0 || filename[0] === '.') {
448 540 dialog.modal({
449 541 title : 'Invalid file name',
450 542 body : "File names must be at least one character and not start with a dot",
451 543 buttons : {'OK' : { 'class' : 'btn-primary' }}
452 544 });
453 545 return false;
454 546 }
455 547 if (filedata instanceof ArrayBuffer) {
456 548 // base64-encode binary file data
457 549 var bytes = '';
458 550 var buf = new Uint8Array(filedata);
459 551 var nbytes = buf.byteLength;
460 552 for (var i=0; i<nbytes; i++) {
461 553 bytes += String.fromCharCode(buf[i]);
462 554 }
463 555 filedata = btoa(bytes);
464 556 format = 'base64';
465 557 }
466 558 var model = {};
467 559
468 560 var name_and_ext = utils.splitext(filename);
469 561 var file_ext = name_and_ext[1];
470 562 var content_type;
471 563 if (file_ext === '.ipynb') {
472 564 model.type = 'notebook';
473 565 model.format = 'json';
474 566 try {
475 567 model.content = JSON.parse(filedata);
476 568 } catch (e) {
477 569 dialog.modal({
478 570 title : 'Cannot upload invalid Notebook',
479 571 body : "The error was: " + e,
480 572 buttons : {'OK' : {
481 573 'class' : 'btn-primary',
482 574 click: function () {
483 575 item.remove();
484 576 }
485 577 }}
486 578 });
487 579 return false;
488 580 }
489 581 content_type = 'application/json';
490 582 } else {
491 583 model.type = 'file';
492 584 model.format = format;
493 585 model.content = filedata;
494 586 content_type = 'application/octet-stream';
495 587 }
496 588 filedata = item.data('filedata');
497 589
498 590 var on_success = function () {
499 591 item.removeClass('new-file');
500 592 that.add_link(model, item);
501 593 that.add_delete_button(item);
502 594 that.session_list.load_sessions();
503 595 };
504 596
505 597 var exists = false;
506 598 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
507 599 if ($(v).data('name') === filename) { exists = true; return false; }
508 600 });
509 601
510 602 if (exists) {
511 603 dialog.modal({
512 604 title : "Replace file",
513 605 body : 'There is already a file named ' + filename + ', do you want to replace it?',
514 606 buttons : {
515 607 Overwrite : {
516 608 class: "btn-danger",
517 609 click: function () {
518 610 that.contents.save(path, model).then(on_success);
519 611 }
520 612 },
521 613 Cancel : {
522 614 click: function() { item.remove(); }
523 615 }
524 616 }
525 617 });
526 618 } else {
527 619 that.contents.save(path, model).then(on_success);
528 620 }
529 621
530 622 return false;
531 623 });
532 624 var cancel_button = $('<button/>').text("Cancel")
533 625 .addClass("btn btn-default btn-xs")
534 626 .click(function (e) {
535 627 item.remove();
536 628 return false;
537 629 });
538 630 item.find(".item_buttons").empty()
539 631 .append(upload_button)
540 632 .append(cancel_button);
541 633 };
542 634
543 635
544 636 // Backwards compatability.
545 637 IPython.NotebookList = NotebookList;
546 638
547 639 return {'NotebookList': NotebookList};
548 640 });
General Comments 0
You need to be logged in to leave comments. Login now