##// END OF EJS Templates
New file open editor
Jonathan Frederic -
Show More
@@ -1,540 +1,546 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 that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'});
75 var w = window.open();
76 that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'}).then(function(data) {
77 var url = utils.url_join_encode(
78 that.base_url, 'edit', data.path
79 );
80 w.location = url;
81 });
76 82 that.load_sessions();
77 83 });
78 84 $('#new-folder').click(function(e) {
79 85 that.contents.new_untitled(that.notebook_path || '', {type: 'directory'});
80 86 that.load_sessions();
81 87 });
82 88 }
83 89 };
84 90
85 91 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
86 92 var that = this;
87 93 var files;
88 94 if(dropOrForm =='drop'){
89 95 files = event.originalEvent.dataTransfer.files;
90 96 } else
91 97 {
92 98 files = event.originalEvent.target.files;
93 99 }
94 100 for (var i = 0; i < files.length; i++) {
95 101 var f = files[i];
96 102 var name_and_ext = utils.splitext(f.name);
97 103 var file_ext = name_and_ext[1];
98 104
99 105 var reader = new FileReader();
100 106 if (file_ext === '.ipynb') {
101 107 reader.readAsText(f);
102 108 } else {
103 109 // read non-notebook files as binary
104 110 reader.readAsArrayBuffer(f);
105 111 }
106 112 var item = that.new_item(0);
107 113 item.addClass('new-file');
108 114 that.add_name_input(f.name, item, file_ext == '.ipynb' ? 'notebook' : 'file');
109 115 // Store the list item in the reader so we can use it later
110 116 // to know which item it belongs to.
111 117 $(reader).data('item', item);
112 118 reader.onload = function (event) {
113 119 var item = $(event.target).data('item');
114 120 that.add_file_data(event.target.result, item);
115 121 that.add_upload_button(item);
116 122 };
117 123 reader.onerror = function (event) {
118 124 var item = $(event.target).data('item');
119 125 var name = item.data('name');
120 126 item.remove();
121 127 dialog.modal({
122 128 title : 'Failed to read file',
123 129 body : "Failed to read file '" + name + "'",
124 130 buttons : {'OK' : { 'class' : 'btn-primary' }}
125 131 });
126 132 };
127 133 }
128 134 // Replace the file input form wth a clone of itself. This is required to
129 135 // reset the form. Otherwise, if you upload a file, delete it and try to
130 136 // upload it again, the changed event won't fire.
131 137 var form = $('input.fileinput');
132 138 form.replaceWith(form.clone(true));
133 139 return false;
134 140 };
135 141
136 142 NotebookList.prototype.clear_list = function (remove_uploads) {
137 143 /**
138 144 * Clears the navigation tree.
139 145 *
140 146 * Parameters
141 147 * remove_uploads: bool=False
142 148 * Should upload prompts also be removed from the tree.
143 149 */
144 150 if (remove_uploads) {
145 151 this.element.children('.list_item').remove();
146 152 } else {
147 153 this.element.children('.list_item:not(.new-file)').remove();
148 154 }
149 155 };
150 156
151 157 NotebookList.prototype.load_sessions = function(){
152 158 this.session_list.load_sessions();
153 159 };
154 160
155 161
156 162 NotebookList.prototype.sessions_loaded = function(data){
157 163 this.sessions = data;
158 164 this.load_list();
159 165 };
160 166
161 167 NotebookList.prototype.load_list = function () {
162 168 var that = this;
163 169 this.contents.list_contents(that.notebook_path).then(
164 170 $.proxy(this.draw_notebook_list, this),
165 171 function(error) {
166 172 that.draw_notebook_list({content: []}, "Server error: " + error.message);
167 173 }
168 174 );
169 175 };
170 176
171 177 /**
172 178 * Draw the list of notebooks
173 179 * @method draw_notebook_list
174 180 * @param {Array} list An array of dictionaries representing files or
175 181 * directories.
176 182 * @param {String} error_msg An error message
177 183 */
178 184
179 185
180 186 var type_order = {'directory':0,'notebook':1,'file':2};
181 187
182 188 NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
183 189 list.content.sort(function(a, b) {
184 190 if (type_order[a['type']] < type_order[b['type']]) {
185 191 return -1;
186 192 }
187 193 if (type_order[a['type']] > type_order[b['type']]) {
188 194 return 1;
189 195 }
190 196 if (a['name'] < b['name']) {
191 197 return -1;
192 198 }
193 199 if (a['name'] > b['name']) {
194 200 return 1;
195 201 }
196 202 return 0;
197 203 });
198 204 var message = error_msg || 'Notebook list empty.';
199 205 var item = null;
200 206 var model = null;
201 207 var len = list.content.length;
202 208 this.clear_list();
203 209 var n_uploads = this.element.children('.list_item').length;
204 210 if (len === 0) {
205 211 item = this.new_item(0);
206 212 var span12 = item.children().first();
207 213 span12.empty();
208 214 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
209 215 }
210 216 var path = this.notebook_path;
211 217 var offset = n_uploads;
212 218 if (path !== '') {
213 219 item = this.new_item(offset);
214 220 model = {
215 221 type: 'directory',
216 222 name: '..',
217 223 path: utils.url_path_split(path)[0],
218 224 };
219 225 this.add_link(model, item);
220 226 offset += 1;
221 227 }
222 228 for (var i=0; i<len; i++) {
223 229 model = list.content[i];
224 230 item = this.new_item(i+offset);
225 231 this.add_link(model, item);
226 232 }
227 233 // Trigger an event when we've finished drawing the notebook list.
228 234 events.trigger('draw_notebook_list.NotebookList');
229 235 };
230 236
231 237
232 238 NotebookList.prototype.new_item = function (index) {
233 239 var item = $('<div/>').addClass("list_item").addClass("row");
234 240 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
235 241 // item.css('border-top-style','none');
236 242 item.append($("<div/>").addClass("col-md-12").append(
237 243 $('<i/>').addClass('item_icon')
238 244 ).append(
239 245 $("<a/>").addClass("item_link").append(
240 246 $("<span/>").addClass("item_name")
241 247 )
242 248 ).append(
243 249 $('<div/>').addClass("item_buttons pull-right")
244 250 ));
245 251
246 252 if (index === -1) {
247 253 this.element.append(item);
248 254 } else {
249 255 this.element.children().eq(index).after(item);
250 256 }
251 257 return item;
252 258 };
253 259
254 260
255 261 NotebookList.icons = {
256 262 directory: 'folder_icon',
257 263 notebook: 'notebook_icon',
258 264 file: 'file_icon',
259 265 };
260 266
261 267 NotebookList.uri_prefixes = {
262 268 directory: 'tree',
263 269 notebook: 'notebooks',
264 270 file: 'edit',
265 271 };
266 272
267 273
268 274 NotebookList.prototype.add_link = function (model, item) {
269 275 var path = model.path,
270 276 name = model.name;
271 277 item.data('name', name);
272 278 item.data('path', path);
273 279 item.find(".item_name").text(name);
274 280 var icon = NotebookList.icons[model.type];
275 281 var uri_prefix = NotebookList.uri_prefixes[model.type];
276 282 item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
277 283 var link = item.find("a.item_link")
278 284 .attr('href',
279 285 utils.url_join_encode(
280 286 this.base_url,
281 287 uri_prefix,
282 288 path
283 289 )
284 290 );
285 291 // directory nav doesn't open new tabs
286 292 // files, notebooks do
287 293 if (model.type !== "directory") {
288 294 link.attr('target','_blank');
289 295 }
290 296 if (model.type !== 'directory') {
291 297 this.add_duplicate_button(item);
292 298 }
293 299 if (model.type == 'file') {
294 300 this.add_delete_button(item);
295 301 } else if (model.type == 'notebook') {
296 302 if (this.sessions[path] === undefined){
297 303 this.add_delete_button(item);
298 304 } else {
299 305 this.add_shutdown_button(item, this.sessions[path]);
300 306 }
301 307 }
302 308 };
303 309
304 310
305 311 NotebookList.prototype.add_name_input = function (name, item, icon_type) {
306 312 item.data('name', name);
307 313 item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
308 314 item.find(".item_name").empty().append(
309 315 $('<input/>')
310 316 .addClass("filename_input")
311 317 .attr('value', name)
312 318 .attr('size', '30')
313 319 .attr('type', 'text')
314 320 .keyup(function(event){
315 321 if(event.keyCode == 13){item.find('.upload_button').click();}
316 322 else if(event.keyCode == 27){item.remove();}
317 323 })
318 324 );
319 325 };
320 326
321 327
322 328 NotebookList.prototype.add_file_data = function (data, item) {
323 329 item.data('filedata', data);
324 330 };
325 331
326 332
327 333 NotebookList.prototype.add_shutdown_button = function (item, session) {
328 334 var that = this;
329 335 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-warning").
330 336 click(function (e) {
331 337 var settings = {
332 338 processData : false,
333 339 cache : false,
334 340 type : "DELETE",
335 341 dataType : "json",
336 342 success : function () {
337 343 that.load_sessions();
338 344 },
339 345 error : utils.log_ajax_error,
340 346 };
341 347 var url = utils.url_join_encode(
342 348 that.base_url,
343 349 'api/sessions',
344 350 session
345 351 );
346 352 $.ajax(url, settings);
347 353 return false;
348 354 });
349 355 item.find(".item_buttons").append(shutdown_button);
350 356 };
351 357
352 358 NotebookList.prototype.add_duplicate_button = function (item) {
353 359 var notebooklist = this;
354 360 var duplicate_button = $("<button/>").text("Duplicate").addClass("btn btn-default btn-xs").
355 361 click(function (e) {
356 362 // $(this) is the button that was clicked.
357 363 var that = $(this);
358 364 var name = item.data('name');
359 365 var path = item.data('path');
360 366 var message = 'Are you sure you want to duplicate ' + name + '?';
361 367 var copy_from = {copy_from : path};
362 368 IPython.dialog.modal({
363 369 title : "Duplicate " + name,
364 370 body : message,
365 371 buttons : {
366 372 Duplicate : {
367 373 class: "btn-primary",
368 374 click: function() {
369 375 notebooklist.contents.copy(path, notebooklist.notebook_path).then(function () {
370 376 notebooklist.load_list();
371 377 });
372 378 }
373 379 },
374 380 Cancel : {}
375 381 }
376 382 });
377 383 return false;
378 384 });
379 385 item.find(".item_buttons").append(duplicate_button);
380 386 };
381 387
382 388 NotebookList.prototype.add_delete_button = function (item) {
383 389 var notebooklist = this;
384 390 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
385 391 click(function (e) {
386 392 // $(this) is the button that was clicked.
387 393 var that = $(this);
388 394 // We use the filename from the parent list_item element's
389 395 // data because the outer scope's values change as we iterate through the loop.
390 396 var parent_item = that.parents('div.list_item');
391 397 var name = parent_item.data('name');
392 398 var path = parent_item.data('path');
393 399 var message = 'Are you sure you want to permanently delete the file: ' + name + '?';
394 400 dialog.modal({
395 401 title : "Delete file",
396 402 body : message,
397 403 buttons : {
398 404 Delete : {
399 405 class: "btn-danger",
400 406 click: function() {
401 407 notebooklist.contents.delete(path).then(
402 408 function() {
403 409 notebooklist.notebook_deleted(path);
404 410 }
405 411 );
406 412 }
407 413 },
408 414 Cancel : {}
409 415 }
410 416 });
411 417 return false;
412 418 });
413 419 item.find(".item_buttons").append(delete_button);
414 420 };
415 421
416 422 NotebookList.prototype.notebook_deleted = function(path) {
417 423 /**
418 424 * Remove the deleted notebook.
419 425 */
420 426 $( ":data(path)" ).each(function() {
421 427 var element = $(this);
422 428 if (element.data("path") == path) {
423 429 element.remove();
424 430 events.trigger('notebook_deleted.NotebookList');
425 431 }
426 432 });
427 433 };
428 434
429 435
430 436 NotebookList.prototype.add_upload_button = function (item) {
431 437 var that = this;
432 438 var upload_button = $('<button/>').text("Upload")
433 439 .addClass('btn btn-primary btn-xs upload_button')
434 440 .click(function (e) {
435 441 var filename = item.find('.item_name > input').val();
436 442 var path = utils.url_path_join(that.notebook_path, filename);
437 443 var filedata = item.data('filedata');
438 444 var format = 'text';
439 445 if (filename.length === 0 || filename[0] === '.') {
440 446 dialog.modal({
441 447 title : 'Invalid file name',
442 448 body : "File names must be at least one character and not start with a dot",
443 449 buttons : {'OK' : { 'class' : 'btn-primary' }}
444 450 });
445 451 return false;
446 452 }
447 453 if (filedata instanceof ArrayBuffer) {
448 454 // base64-encode binary file data
449 455 var bytes = '';
450 456 var buf = new Uint8Array(filedata);
451 457 var nbytes = buf.byteLength;
452 458 for (var i=0; i<nbytes; i++) {
453 459 bytes += String.fromCharCode(buf[i]);
454 460 }
455 461 filedata = btoa(bytes);
456 462 format = 'base64';
457 463 }
458 464 var model = {};
459 465
460 466 var name_and_ext = utils.splitext(filename);
461 467 var file_ext = name_and_ext[1];
462 468 var content_type;
463 469 if (file_ext === '.ipynb') {
464 470 model.type = 'notebook';
465 471 model.format = 'json';
466 472 try {
467 473 model.content = JSON.parse(filedata);
468 474 } catch (e) {
469 475 dialog.modal({
470 476 title : 'Cannot upload invalid Notebook',
471 477 body : "The error was: " + e,
472 478 buttons : {'OK' : {
473 479 'class' : 'btn-primary',
474 480 click: function () {
475 481 item.remove();
476 482 }
477 483 }}
478 484 });
479 485 return false;
480 486 }
481 487 content_type = 'application/json';
482 488 } else {
483 489 model.type = 'file';
484 490 model.format = format;
485 491 model.content = filedata;
486 492 content_type = 'application/octet-stream';
487 493 }
488 494 filedata = item.data('filedata');
489 495
490 496 var on_success = function () {
491 497 item.removeClass('new-file');
492 498 that.add_link(model, item);
493 499 that.add_delete_button(item);
494 500 that.session_list.load_sessions();
495 501 };
496 502
497 503 var exists = false;
498 504 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
499 505 if ($(v).data('name') === filename) { exists = true; return false; }
500 506 });
501 507
502 508 if (exists) {
503 509 dialog.modal({
504 510 title : "Replace file",
505 511 body : 'There is already a file named ' + filename + ', do you want to replace it?',
506 512 buttons : {
507 513 Overwrite : {
508 514 class: "btn-danger",
509 515 click: function () {
510 516 that.contents.save(path, model).then(on_success);
511 517 }
512 518 },
513 519 Cancel : {
514 520 click: function() { item.remove(); }
515 521 }
516 522 }
517 523 });
518 524 } else {
519 525 that.contents.save(path, model).then(on_success);
520 526 }
521 527
522 528 return false;
523 529 });
524 530 var cancel_button = $('<button/>').text("Cancel")
525 531 .addClass("btn btn-default btn-xs")
526 532 .click(function (e) {
527 533 item.remove();
528 534 return false;
529 535 });
530 536 item.find(".item_buttons").empty()
531 537 .append(upload_button)
532 538 .append(cancel_button);
533 539 };
534 540
535 541
536 542 // Backwards compatability.
537 543 IPython.NotebookList = NotebookList;
538 544
539 545 return {'NotebookList': NotebookList};
540 546 });
General Comments 0
You need to be logged in to leave comments. Login now