##// END OF EJS Templates
DEV: Add various events.
Scott Sanderson -
Show More
@@ -1,569 +1,570
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3 /**
4 4 *
5 5 *
6 6 * @module codecell
7 7 * @namespace codecell
8 8 * @class CodeCell
9 9 */
10 10
11 11
12 12 define([
13 13 'base/js/namespace',
14 14 'jquery',
15 15 'base/js/utils',
16 16 'base/js/keyboard',
17 17 'notebook/js/cell',
18 18 'notebook/js/outputarea',
19 19 'notebook/js/completer',
20 20 'notebook/js/celltoolbar',
21 21 'codemirror/lib/codemirror',
22 22 'codemirror/mode/python/python',
23 23 'notebook/js/codemirror-ipython'
24 24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
25 25 "use strict";
26 26
27 27 var Cell = cell.Cell;
28 28
29 29 /* local util for codemirror */
30 30 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
31 31
32 32 /**
33 33 *
34 34 * function to delete until previous non blanking space character
35 35 * or first multiple of 4 tabstop.
36 36 * @private
37 37 */
38 38 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
39 39 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
40 40 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
41 41 var cur = cm.getCursor(), line = cm.getLine(cur.line);
42 42 var tabsize = cm.getOption('tabSize');
43 43 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
44 44 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
45 45 var select = cm.getRange(from,cur);
46 46 if( select.match(/^\ +$/) !== null){
47 47 cm.replaceRange("",from,cur);
48 48 } else {
49 49 cm.deleteH(-1,"char");
50 50 }
51 51 };
52 52
53 53 var keycodes = keyboard.keycodes;
54 54
55 55 var CodeCell = function (kernel, options) {
56 56 // Constructor
57 57 //
58 58 // A Cell conceived to write code.
59 59 //
60 60 // Parameters:
61 61 // kernel: Kernel instance
62 62 // The kernel doesn't have to be set at creation time, in that case
63 63 // it will be null and set_kernel has to be called later.
64 64 // options: dictionary
65 65 // Dictionary of keyword arguments.
66 66 // events: $(Events) instance
67 67 // config: dictionary
68 68 // keyboard_manager: KeyboardManager instance
69 69 // notebook: Notebook instance
70 70 // tooltip: Tooltip instance
71 71 this.kernel = kernel || null;
72 72 this.notebook = options.notebook;
73 73 this.collapsed = false;
74 74 this.events = options.events;
75 75 this.tooltip = options.tooltip;
76 76 this.config = options.config;
77 77
78 78 // create all attributed in constructor function
79 79 // even if null for V8 VM optimisation
80 80 this.input_prompt_number = null;
81 81 this.celltoolbar = null;
82 82 this.output_area = null;
83 83 // Keep a stack of the 'active' output areas (where active means the
84 84 // output area that recieves output). When a user activates an output
85 85 // area, it gets pushed to the stack. Then, when the output area is
86 86 // deactivated, it's popped from the stack. When the stack is empty,
87 87 // the cell's output area is used.
88 88 this.active_output_areas = [];
89 89 var that = this;
90 90 Object.defineProperty(this, 'active_output_area', {
91 91 get: function() {
92 92 if (that.active_output_areas && that.active_output_areas.length > 0) {
93 93 return that.active_output_areas[that.active_output_areas.length-1];
94 94 } else {
95 95 return that.output_area;
96 96 }
97 97 },
98 98 });
99 99
100 100 this.last_msg_id = null;
101 101 this.completer = null;
102 102
103 103
104 104 var config = utils.mergeopt(CodeCell, this.config);
105 105 Cell.apply(this,[{
106 106 config: config,
107 107 keyboard_manager: options.keyboard_manager,
108 108 events: this.events}]);
109 109
110 110 // Attributes we want to override in this subclass.
111 111 this.cell_type = "code";
112 112 this.element.focusout(
113 113 function() { that.auto_highlight(); }
114 114 );
115 115 };
116 116
117 117 CodeCell.options_default = {
118 118 cm_config : {
119 119 extraKeys: {
120 120 "Tab" : "indentMore",
121 121 "Shift-Tab" : "indentLess",
122 122 "Backspace" : "delSpaceToPrevTabStop",
123 123 "Cmd-/" : "toggleComment",
124 124 "Ctrl-/" : "toggleComment"
125 125 },
126 126 mode: 'ipython',
127 127 theme: 'ipython',
128 128 matchBrackets: true
129 129 }
130 130 };
131 131
132 132 CodeCell.msg_cells = {};
133 133
134 134 CodeCell.prototype = Object.create(Cell.prototype);
135 135
136 136 /**
137 137 * @method push_output_area
138 138 */
139 139 CodeCell.prototype.push_output_area = function (output_area) {
140 140 this.active_output_areas.push(output_area);
141 141 };
142 142
143 143 /**
144 144 * @method pop_output_area
145 145 */
146 146 CodeCell.prototype.pop_output_area = function (output_area) {
147 147 var index = this.active_output_areas.lastIndexOf(output_area);
148 148 if (index > -1) {
149 149 this.active_output_areas.splice(index, 1);
150 150 }
151 151 };
152 152
153 153 /**
154 154 * @method auto_highlight
155 155 */
156 156 CodeCell.prototype.auto_highlight = function () {
157 157 this._auto_highlight(this.config.cell_magic_highlight);
158 158 };
159 159
160 160 /** @method create_element */
161 161 CodeCell.prototype.create_element = function () {
162 162 Cell.prototype.create_element.apply(this, arguments);
163 163
164 164 var cell = $('<div></div>').addClass('cell code_cell');
165 165 cell.attr('tabindex','2');
166 166
167 167 var input = $('<div></div>').addClass('input');
168 168 var prompt = $('<div/>').addClass('prompt input_prompt');
169 169 var inner_cell = $('<div/>').addClass('inner_cell');
170 170 this.celltoolbar = new celltoolbar.CellToolbar({
171 171 cell: this,
172 172 notebook: this.notebook});
173 173 inner_cell.append(this.celltoolbar.element);
174 174 var input_area = $('<div/>').addClass('input_area');
175 175 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
176 176 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
177 177 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
178 178 inner_cell.append(input_area);
179 179 input.append(prompt).append(inner_cell);
180 180
181 181 var widget_area = $('<div/>')
182 182 .addClass('widget-area')
183 183 .hide();
184 184 this.widget_area = widget_area;
185 185 var widget_prompt = $('<div/>')
186 186 .addClass('prompt')
187 187 .appendTo(widget_area);
188 188 var widget_subarea = $('<div/>')
189 189 .addClass('widget-subarea')
190 190 .appendTo(widget_area);
191 191 this.widget_subarea = widget_subarea;
192 192 var widget_clear_buton = $('<button />')
193 193 .addClass('close')
194 194 .html('&times;')
195 195 .click(function() {
196 196 widget_area.slideUp('', function(){ widget_subarea.html(''); });
197 197 })
198 198 .appendTo(widget_prompt);
199 199
200 200 var output = $('<div></div>');
201 201 cell.append(input).append(widget_area).append(output);
202 202 this.element = cell;
203 203 this.output_area = new outputarea.OutputArea({
204 204 selector: output,
205 205 prompt_area: true,
206 206 events: this.events,
207 207 keyboard_manager: this.keyboard_manager});
208 208 this.completer = new completer.Completer(this, this.events);
209 209 };
210 210
211 211 /** @method bind_events */
212 212 CodeCell.prototype.bind_events = function () {
213 213 Cell.prototype.bind_events.apply(this);
214 214 var that = this;
215 215
216 216 this.element.focusout(
217 217 function() { that.auto_highlight(); }
218 218 );
219 219 };
220 220
221 221
222 222 /**
223 223 * This method gets called in CodeMirror's onKeyDown/onKeyPress
224 224 * handlers and is used to provide custom key handling. Its return
225 225 * value is used to determine if CodeMirror should ignore the event:
226 226 * true = ignore, false = don't ignore.
227 227 * @method handle_codemirror_keyevent
228 228 */
229 229 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
230 230
231 231 var that = this;
232 232 // whatever key is pressed, first, cancel the tooltip request before
233 233 // they are sent, and remove tooltip if any, except for tab again
234 234 var tooltip_closed = null;
235 235 if (event.type === 'keydown' && event.which != keycodes.tab ) {
236 236 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
237 237 }
238 238
239 239 var cur = editor.getCursor();
240 240 if (event.keyCode === keycodes.enter){
241 241 this.auto_highlight();
242 242 }
243 243
244 244 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
245 245 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
246 246 // browser and keyboard layout !
247 247 // Pressing '(' , request tooltip, don't forget to reappend it
248 248 // The second argument says to hide the tooltip if the docstring
249 249 // is actually empty
250 250 this.tooltip.pending(that, true);
251 251 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
252 252 // If tooltip is active, cancel it. The call to
253 253 // remove_and_cancel_tooltip above doesn't pass, force=true.
254 254 // Because of this it won't actually close the tooltip
255 255 // if it is in sticky mode. Thus, we have to check again if it is open
256 256 // and close it with force=true.
257 257 if (!this.tooltip._hidden) {
258 258 this.tooltip.remove_and_cancel_tooltip(true);
259 259 }
260 260 // If we closed the tooltip, don't let CM or the global handlers
261 261 // handle this event.
262 262 event.codemirrorIgnore = true;
263 263 event.preventDefault();
264 264 return true;
265 265 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
266 266 if (editor.somethingSelected() || editor.getSelections().length !== 1){
267 267 var anchor = editor.getCursor("anchor");
268 268 var head = editor.getCursor("head");
269 269 if( anchor.line != head.line){
270 270 return false;
271 271 }
272 272 }
273 273 this.tooltip.request(that);
274 274 event.codemirrorIgnore = true;
275 275 event.preventDefault();
276 276 return true;
277 277 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
278 278 // Tab completion.
279 279 this.tooltip.remove_and_cancel_tooltip();
280 280
281 281 // completion does not work on multicursor, it might be possible though in some cases
282 282 if (editor.somethingSelected() || editor.getSelections().length > 1) {
283 283 return false;
284 284 }
285 285 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
286 286 if (pre_cursor.trim() === "") {
287 287 // Don't autocomplete if the part of the line before the cursor
288 288 // is empty. In this case, let CodeMirror handle indentation.
289 289 return false;
290 290 } else {
291 291 event.codemirrorIgnore = true;
292 292 event.preventDefault();
293 293 this.completer.startCompletion();
294 294 return true;
295 295 }
296 296 }
297 297
298 298 // keyboard event wasn't one of those unique to code cells, let's see
299 299 // if it's one of the generic ones (i.e. check edit mode shortcuts)
300 300 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
301 301 };
302 302
303 303 // Kernel related calls.
304 304
305 305 CodeCell.prototype.set_kernel = function (kernel) {
306 306 this.kernel = kernel;
307 307 };
308 308
309 309 /**
310 310 * Execute current code cell to the kernel
311 311 * @method execute
312 312 */
313 313 CodeCell.prototype.execute = function () {
314 314 if (!this.kernel || !this.kernel.is_connected()) {
315 315 console.log("Can't execute, kernel is not connected.");
316 316 return;
317 317 }
318 318
319 319 this.active_output_area.clear_output();
320 320
321 321 // Clear widget area
322 322 this.widget_subarea.html('');
323 323 this.widget_subarea.height('');
324 324 this.widget_area.height('');
325 325 this.widget_area.hide();
326 326
327 327 this.set_input_prompt('*');
328 328 this.element.addClass("running");
329 329 if (this.last_msg_id) {
330 330 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
331 331 }
332 332 var callbacks = this.get_callbacks();
333 333
334 334 var old_msg_id = this.last_msg_id;
335 335 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
336 336 if (old_msg_id) {
337 337 delete CodeCell.msg_cells[old_msg_id];
338 338 }
339 339 CodeCell.msg_cells[this.last_msg_id] = this;
340 340 this.render();
341 this.events.trigger('execute.CodeCell');
341 342 };
342 343
343 344 /**
344 345 * Construct the default callbacks for
345 346 * @method get_callbacks
346 347 */
347 348 CodeCell.prototype.get_callbacks = function () {
348 349 var that = this;
349 350 return {
350 351 shell : {
351 352 reply : $.proxy(this._handle_execute_reply, this),
352 353 payload : {
353 354 set_next_input : $.proxy(this._handle_set_next_input, this),
354 355 page : $.proxy(this._open_with_pager, this)
355 356 }
356 357 },
357 358 iopub : {
358 359 output : function() {
359 360 that.active_output_area.handle_output.apply(that.active_output_area, arguments);
360 361 },
361 362 clear_output : function() {
362 363 that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments);
363 364 },
364 365 },
365 366 input : $.proxy(this._handle_input_request, this)
366 367 };
367 368 };
368 369
369 370 CodeCell.prototype._open_with_pager = function (payload) {
370 371 this.events.trigger('open_with_text.Pager', payload);
371 372 };
372 373
373 374 /**
374 375 * @method _handle_execute_reply
375 376 * @private
376 377 */
377 378 CodeCell.prototype._handle_execute_reply = function (msg) {
378 379 this.set_input_prompt(msg.content.execution_count);
379 380 this.element.removeClass("running");
380 381 this.events.trigger('set_dirty.Notebook', {value: true});
381 382 };
382 383
383 384 /**
384 385 * @method _handle_set_next_input
385 386 * @private
386 387 */
387 388 CodeCell.prototype._handle_set_next_input = function (payload) {
388 389 var data = {'cell': this, 'text': payload.text};
389 390 this.events.trigger('set_next_input.Notebook', data);
390 391 };
391 392
392 393 /**
393 394 * @method _handle_input_request
394 395 * @private
395 396 */
396 397 CodeCell.prototype._handle_input_request = function (msg) {
397 398 this.active_output_area.append_raw_input(msg);
398 399 };
399 400
400 401
401 402 // Basic cell manipulation.
402 403
403 404 CodeCell.prototype.select = function () {
404 405 var cont = Cell.prototype.select.apply(this);
405 406 if (cont) {
406 407 this.code_mirror.refresh();
407 408 this.auto_highlight();
408 409 }
409 410 return cont;
410 411 };
411 412
412 413 CodeCell.prototype.render = function () {
413 414 var cont = Cell.prototype.render.apply(this);
414 415 // Always execute, even if we are already in the rendered state
415 416 return cont;
416 417 };
417 418
418 419 CodeCell.prototype.select_all = function () {
419 420 var start = {line: 0, ch: 0};
420 421 var nlines = this.code_mirror.lineCount();
421 422 var last_line = this.code_mirror.getLine(nlines-1);
422 423 var end = {line: nlines-1, ch: last_line.length};
423 424 this.code_mirror.setSelection(start, end);
424 425 };
425 426
426 427
427 428 CodeCell.prototype.collapse_output = function () {
428 429 this.output_area.collapse();
429 430 };
430 431
431 432
432 433 CodeCell.prototype.expand_output = function () {
433 434 this.output_area.expand();
434 435 this.output_area.unscroll_area();
435 436 };
436 437
437 438 CodeCell.prototype.scroll_output = function () {
438 439 this.output_area.expand();
439 440 this.output_area.scroll_if_long();
440 441 };
441 442
442 443 CodeCell.prototype.toggle_output = function () {
443 444 this.output_area.toggle_output();
444 445 };
445 446
446 447 CodeCell.prototype.toggle_output_scroll = function () {
447 448 this.output_area.toggle_scroll();
448 449 };
449 450
450 451
451 452 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
452 453 var ns;
453 454 if (prompt_value === undefined || prompt_value === null) {
454 455 ns = "&nbsp;";
455 456 } else {
456 457 ns = encodeURIComponent(prompt_value);
457 458 }
458 459 return 'In&nbsp;[' + ns + ']:';
459 460 };
460 461
461 462 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
462 463 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
463 464 for(var i=1; i < lines_number; i++) {
464 465 html.push(['...:']);
465 466 }
466 467 return html.join('<br/>');
467 468 };
468 469
469 470 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
470 471
471 472
472 473 CodeCell.prototype.set_input_prompt = function (number) {
473 474 var nline = 1;
474 475 if (this.code_mirror !== undefined) {
475 476 nline = this.code_mirror.lineCount();
476 477 }
477 478 this.input_prompt_number = number;
478 479 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
479 480 // This HTML call is okay because the user contents are escaped.
480 481 this.element.find('div.input_prompt').html(prompt_html);
481 482 };
482 483
483 484
484 485 CodeCell.prototype.clear_input = function () {
485 486 this.code_mirror.setValue('');
486 487 };
487 488
488 489
489 490 CodeCell.prototype.get_text = function () {
490 491 return this.code_mirror.getValue();
491 492 };
492 493
493 494
494 495 CodeCell.prototype.set_text = function (code) {
495 496 return this.code_mirror.setValue(code);
496 497 };
497 498
498 499
499 500 CodeCell.prototype.clear_output = function (wait) {
500 501 this.active_output_area.clear_output(wait);
501 502 this.set_input_prompt();
502 503 };
503 504
504 505
505 506 // JSON serialization
506 507
507 508 CodeCell.prototype.fromJSON = function (data) {
508 509 Cell.prototype.fromJSON.apply(this, arguments);
509 510 if (data.cell_type === 'code') {
510 511 if (data.source !== undefined) {
511 512 this.set_text(data.source);
512 513 // make this value the starting point, so that we can only undo
513 514 // to this state, instead of a blank cell
514 515 this.code_mirror.clearHistory();
515 516 this.auto_highlight();
516 517 }
517 518 this.set_input_prompt(data.execution_count);
518 519 this.output_area.trusted = data.metadata.trusted || false;
519 520 this.output_area.fromJSON(data.outputs);
520 521 if (data.metadata.collapsed !== undefined) {
521 522 if (data.metadata.collapsed) {
522 523 this.collapse_output();
523 524 } else {
524 525 this.expand_output();
525 526 }
526 527 }
527 528 }
528 529 };
529 530
530 531
531 532 CodeCell.prototype.toJSON = function () {
532 533 var data = Cell.prototype.toJSON.apply(this);
533 534 data.source = this.get_text();
534 535 // is finite protect against undefined and '*' value
535 536 if (isFinite(this.input_prompt_number)) {
536 537 data.execution_count = this.input_prompt_number;
537 538 } else {
538 539 data.execution_count = null;
539 540 }
540 541 var outputs = this.output_area.toJSON();
541 542 data.outputs = outputs;
542 543 data.metadata.trusted = this.output_area.trusted;
543 544 data.metadata.collapsed = this.output_area.collapsed;
544 545 return data;
545 546 };
546 547
547 548 /**
548 549 * handle cell level logic when a cell is unselected
549 550 * @method unselect
550 551 * @return is the action being taken
551 552 */
552 553 CodeCell.prototype.unselect = function () {
553 554 var cont = Cell.prototype.unselect.apply(this);
554 555 if (cont) {
555 556 // When a code cell is usnelected, make sure that the corresponding
556 557 // tooltip and completer to that cell is closed.
557 558 this.tooltip.remove_and_cancel_tooltip(true);
558 559 if (this.completer !== null) {
559 560 this.completer.close();
560 561 }
561 562 }
562 563 return cont;
563 564 };
564 565
565 566 // Backwards compatability.
566 567 IPython.CodeCell = CodeCell;
567 568
568 569 return {'CodeCell': CodeCell};
569 570 });
@@ -1,498 +1,502
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 ], function(IPython, $, utils, dialog) {
9 'base/js/events',
10 ], function(IPython, $, utils, dialog, events) {
10 11 "use strict";
11 12
12 13 var NotebookList = function (selector, options) {
13 14 // Constructor
14 15 //
15 16 // Parameters:
16 17 // selector: string
17 18 // options: dictionary
18 19 // Dictionary of keyword arguments.
19 20 // session_list: SessionList instance
20 21 // element_name: string
21 22 // base_url: string
22 23 // notebook_path: string
23 24 // contents: Contents instance
24 25 var that = this;
25 26 this.session_list = options.session_list;
26 27 // allow code re-use by just changing element_name in kernellist.js
27 28 this.element_name = options.element_name || 'notebook';
28 29 this.selector = selector;
29 30 if (this.selector !== undefined) {
30 31 this.element = $(selector);
31 32 this.style();
32 33 this.bind_events();
33 34 }
34 35 this.notebooks_list = [];
35 36 this.sessions = {};
36 37 this.base_url = options.base_url || utils.get_body_data("baseUrl");
37 38 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
38 39 this.contents = options.contents;
39 40 if (this.session_list && this.session_list.events) {
40 41 this.session_list.events.on('sessions_loaded.Dashboard',
41 42 function(e, d) { that.sessions_loaded(d); });
42 43 }
43 44 };
44 45
45 46 NotebookList.prototype.style = function () {
46 47 var prefix = '#' + this.element_name;
47 48 $(prefix + '_toolbar').addClass('list_toolbar');
48 49 $(prefix + '_list_info').addClass('toolbar_info');
49 50 $(prefix + '_buttons').addClass('toolbar_buttons');
50 51 $(prefix + '_list_header').addClass('list_header');
51 52 this.element.addClass("list_container");
52 53 };
53 54
54 55
55 56 NotebookList.prototype.bind_events = function () {
56 57 var that = this;
57 58 $('#refresh_' + this.element_name + '_list').click(function () {
58 59 that.load_sessions();
59 60 });
60 61 this.element.bind('dragover', function () {
61 62 return false;
62 63 });
63 64 this.element.bind('drop', function(event){
64 65 that.handleFilesUpload(event,'drop');
65 66 return false;
66 67 });
67 68 };
68 69
69 70 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
70 71 var that = this;
71 72 var files;
72 73 if(dropOrForm =='drop'){
73 74 files = event.originalEvent.dataTransfer.files;
74 75 } else
75 76 {
76 77 files = event.originalEvent.target.files;
77 78 }
78 79 for (var i = 0; i < files.length; i++) {
79 80 var f = files[i];
80 81 var name_and_ext = utils.splitext(f.name);
81 82 var file_ext = name_and_ext[1];
82 83
83 84 var reader = new FileReader();
84 85 if (file_ext === '.ipynb') {
85 86 reader.readAsText(f);
86 87 } else {
87 88 // read non-notebook files as binary
88 89 reader.readAsArrayBuffer(f);
89 90 }
90 91 var item = that.new_item(0);
91 92 item.addClass('new-file');
92 93 that.add_name_input(f.name, item, file_ext == '.ipynb' ? 'notebook' : 'file');
93 94 // Store the list item in the reader so we can use it later
94 95 // to know which item it belongs to.
95 96 $(reader).data('item', item);
96 97 reader.onload = function (event) {
97 98 var item = $(event.target).data('item');
98 99 that.add_file_data(event.target.result, item);
99 100 that.add_upload_button(item);
100 101 };
101 102 reader.onerror = function (event) {
102 103 var item = $(event.target).data('item');
103 104 var name = item.data('name');
104 105 item.remove();
105 106 dialog.modal({
106 107 title : 'Failed to read file',
107 108 body : "Failed to read file '" + name + "'",
108 109 buttons : {'OK' : { 'class' : 'btn-primary' }}
109 110 });
110 111 };
111 112 }
112 113 // Replace the file input form wth a clone of itself. This is required to
113 114 // reset the form. Otherwise, if you upload a file, delete it and try to
114 115 // upload it again, the changed event won't fire.
115 116 var form = $('input.fileinput');
116 117 form.replaceWith(form.clone(true));
117 118 return false;
118 119 };
119 120
120 121 NotebookList.prototype.clear_list = function (remove_uploads) {
121 122 // Clears the navigation tree.
122 123 //
123 124 // Parameters
124 125 // remove_uploads: bool=False
125 126 // Should upload prompts also be removed from the tree.
126 127 if (remove_uploads) {
127 128 this.element.children('.list_item').remove();
128 129 } else {
129 130 this.element.children('.list_item:not(.new-file)').remove();
130 131 }
131 132 };
132 133
133 134 NotebookList.prototype.load_sessions = function(){
134 135 this.session_list.load_sessions();
135 136 };
136 137
137 138
138 139 NotebookList.prototype.sessions_loaded = function(data){
139 140 this.sessions = data;
140 141 this.load_list();
141 142 };
142 143
143 144 NotebookList.prototype.load_list = function () {
144 145 var that = this;
145 146 this.contents.list_contents(that.notebook_path).then(
146 147 $.proxy(this.draw_notebook_list, this),
147 148 function(error) {
148 149 that.draw_notebook_list({content: []}, "Server error: " + error.message);
149 150 }
150 151 );
151 152 };
152 153
153 154 /**
154 155 * Draw the list of notebooks
155 156 * @method draw_notebook_list
156 157 * @param {Array} list An array of dictionaries representing files or
157 158 * directories.
158 159 * @param {String} error_msg An error message
159 160 */
160 161 NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
161 162 var message = error_msg || 'Notebook list empty.';
162 163 var item = null;
163 164 var model = null;
164 165 var len = list.content.length;
165 166 this.clear_list();
166 167 var n_uploads = this.element.children('.list_item').length;
167 168 if (len === 0) {
168 169 item = this.new_item(0);
169 170 var span12 = item.children().first();
170 171 span12.empty();
171 172 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
172 173 }
173 174 var path = this.notebook_path;
174 175 var offset = n_uploads;
175 176 if (path !== '') {
176 177 item = this.new_item(offset);
177 178 model = {
178 179 type: 'directory',
179 180 name: '..',
180 181 path: utils.url_path_split(path)[0],
181 182 };
182 183 this.add_link(model, item);
183 184 offset += 1;
184 185 }
185 186 for (var i=0; i<len; i++) {
186 187 model = list.content[i];
187 188 item = this.new_item(i+offset);
188 189 this.add_link(model, item);
189 190 }
191 // Trigger an event when we've finished drawing the notebook list.
192 events.trigger('draw_notebook_list.NotebookList');
190 193 };
191 194
192 195
193 196 NotebookList.prototype.new_item = function (index) {
194 197 var item = $('<div/>').addClass("list_item").addClass("row");
195 198 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
196 199 // item.css('border-top-style','none');
197 200 item.append($("<div/>").addClass("col-md-12").append(
198 201 $('<i/>').addClass('item_icon')
199 202 ).append(
200 203 $("<a/>").addClass("item_link").append(
201 204 $("<span/>").addClass("item_name")
202 205 )
203 206 ).append(
204 207 $('<div/>').addClass("item_buttons btn-group pull-right")
205 208 ));
206 209
207 210 if (index === -1) {
208 211 this.element.append(item);
209 212 } else {
210 213 this.element.children().eq(index).after(item);
211 214 }
212 215 return item;
213 216 };
214 217
215 218
216 219 NotebookList.icons = {
217 220 directory: 'folder_icon',
218 221 notebook: 'notebook_icon',
219 222 file: 'file_icon',
220 223 };
221 224
222 225 NotebookList.uri_prefixes = {
223 226 directory: 'tree',
224 227 notebook: 'notebooks',
225 228 file: 'files',
226 229 };
227 230
228 231
229 232 NotebookList.prototype.add_link = function (model, item) {
230 233 var path = model.path,
231 234 name = model.name;
232 235 item.data('name', name);
233 236 item.data('path', path);
234 237 item.find(".item_name").text(name);
235 238 var icon = NotebookList.icons[model.type];
236 239 var uri_prefix = NotebookList.uri_prefixes[model.type];
237 240 item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
238 241 var link = item.find("a.item_link")
239 242 .attr('href',
240 243 utils.url_join_encode(
241 244 this.base_url,
242 245 uri_prefix,
243 246 path
244 247 )
245 248 );
246 249 // directory nav doesn't open new tabs
247 250 // files, notebooks do
248 251 if (model.type !== "directory") {
249 252 link.attr('target','_blank');
250 253 }
251 254 if (model.type !== 'directory') {
252 255 this.add_duplicate_button(item);
253 256 }
254 257 if (model.type == 'file') {
255 258 this.add_delete_button(item);
256 259 } else if (model.type == 'notebook') {
257 260 if (this.sessions[path] === undefined){
258 261 this.add_delete_button(item);
259 262 } else {
260 263 this.add_shutdown_button(item, this.sessions[path]);
261 264 }
262 265 }
263 266 };
264 267
265 268
266 269 NotebookList.prototype.add_name_input = function (name, item, icon_type) {
267 270 item.data('name', name);
268 271 item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
269 272 item.find(".item_name").empty().append(
270 273 $('<input/>')
271 274 .addClass("filename_input")
272 275 .attr('value', name)
273 276 .attr('size', '30')
274 277 .attr('type', 'text')
275 278 .keyup(function(event){
276 279 if(event.keyCode == 13){item.find('.upload_button').click();}
277 280 else if(event.keyCode == 27){item.remove();}
278 281 })
279 282 );
280 283 };
281 284
282 285
283 286 NotebookList.prototype.add_file_data = function (data, item) {
284 287 item.data('filedata', data);
285 288 };
286 289
287 290
288 291 NotebookList.prototype.add_shutdown_button = function (item, session) {
289 292 var that = this;
290 293 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
291 294 click(function (e) {
292 295 var settings = {
293 296 processData : false,
294 297 cache : false,
295 298 type : "DELETE",
296 299 dataType : "json",
297 300 success : function () {
298 301 that.load_sessions();
299 302 },
300 303 error : utils.log_ajax_error,
301 304 };
302 305 var url = utils.url_join_encode(
303 306 that.base_url,
304 307 'api/sessions',
305 308 session
306 309 );
307 310 $.ajax(url, settings);
308 311 return false;
309 312 });
310 313 item.find(".item_buttons").append(shutdown_button);
311 314 };
312 315
313 316 NotebookList.prototype.add_duplicate_button = function (item) {
314 317 var notebooklist = this;
315 318 var duplicate_button = $("<button/>").text("Duplicate").addClass("btn btn-default btn-xs").
316 319 click(function (e) {
317 320 // $(this) is the button that was clicked.
318 321 var that = $(this);
319 322 var name = item.data('name');
320 323 var path = item.data('path');
321 324 var message = 'Are you sure you want to duplicate ' + name + '?';
322 325 var copy_from = {copy_from : path};
323 326 IPython.dialog.modal({
324 327 title : "Duplicate " + name,
325 328 body : message,
326 329 buttons : {
327 330 Duplicate : {
328 331 class: "btn-primary",
329 332 click: function() {
330 333 notebooklist.contents.copy(path, notebooklist.notebook_path).then(function () {
331 334 notebooklist.load_list();
332 335 });
333 336 }
334 337 },
335 338 Cancel : {}
336 339 }
337 340 });
338 341 return false;
339 342 });
340 343 item.find(".item_buttons").append(duplicate_button);
341 344 };
342 345
343 346 NotebookList.prototype.add_delete_button = function (item) {
344 347 var notebooklist = this;
345 348 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
346 349 click(function (e) {
347 350 // $(this) is the button that was clicked.
348 351 var that = $(this);
349 352 // We use the filename from the parent list_item element's
350 353 // data because the outer scope's values change as we iterate through the loop.
351 354 var parent_item = that.parents('div.list_item');
352 355 var name = parent_item.data('name');
353 356 var path = parent_item.data('path');
354 357 var message = 'Are you sure you want to permanently delete the file: ' + name + '?';
355 358 dialog.modal({
356 359 title : "Delete file",
357 360 body : message,
358 361 buttons : {
359 362 Delete : {
360 363 class: "btn-danger",
361 364 click: function() {
362 365 notebooklist.contents.delete(path).then(
363 366 function() {
364 367 notebooklist.notebook_deleted(path);
365 368 }
366 369 );
367 370 }
368 371 },
369 372 Cancel : {}
370 373 }
371 374 });
372 375 return false;
373 376 });
374 377 item.find(".item_buttons").append(delete_button);
375 378 };
376 379
377 380 NotebookList.prototype.notebook_deleted = function(path) {
378 381 // Remove the deleted notebook.
379 382 $( ":data(path)" ).each(function() {
380 383 var element = $(this);
381 384 if (element.data("path") == path) {
382 385 element.remove();
386 events.trigger('notebook_deleted.NotebookList');
383 387 }
384 388 });
385 389 };
386 390
387 391
388 392 NotebookList.prototype.add_upload_button = function (item) {
389 393 var that = this;
390 394 var upload_button = $('<button/>').text("Upload")
391 395 .addClass('btn btn-primary btn-xs upload_button')
392 396 .click(function (e) {
393 397 var filename = item.find('.item_name > input').val();
394 398 var path = utils.url_path_join(that.notebook_path, filename);
395 399 var filedata = item.data('filedata');
396 400 var format = 'text';
397 401 if (filename.length === 0 || filename[0] === '.') {
398 402 dialog.modal({
399 403 title : 'Invalid file name',
400 404 body : "File names must be at least one character and not start with a dot",
401 405 buttons : {'OK' : { 'class' : 'btn-primary' }}
402 406 });
403 407 return false;
404 408 }
405 409 if (filedata instanceof ArrayBuffer) {
406 410 // base64-encode binary file data
407 411 var bytes = '';
408 412 var buf = new Uint8Array(filedata);
409 413 var nbytes = buf.byteLength;
410 414 for (var i=0; i<nbytes; i++) {
411 415 bytes += String.fromCharCode(buf[i]);
412 416 }
413 417 filedata = btoa(bytes);
414 418 format = 'base64';
415 419 }
416 420 var model = {};
417 421
418 422 var name_and_ext = utils.splitext(filename);
419 423 var file_ext = name_and_ext[1];
420 424 var content_type;
421 425 if (file_ext === '.ipynb') {
422 426 model.type = 'notebook';
423 427 model.format = 'json';
424 428 try {
425 429 model.content = JSON.parse(filedata);
426 430 } catch (e) {
427 431 dialog.modal({
428 432 title : 'Cannot upload invalid Notebook',
429 433 body : "The error was: " + e,
430 434 buttons : {'OK' : {
431 435 'class' : 'btn-primary',
432 436 click: function () {
433 437 item.remove();
434 438 }
435 439 }}
436 440 });
437 441 return false;
438 442 }
439 443 content_type = 'application/json';
440 444 } else {
441 445 model.type = 'file';
442 446 model.format = format;
443 447 model.content = filedata;
444 448 content_type = 'application/octet-stream';
445 449 }
446 450 filedata = item.data('filedata');
447 451
448 452 var on_success = function () {
449 453 item.removeClass('new-file');
450 454 that.add_link(model, item);
451 455 that.add_delete_button(item);
452 456 that.session_list.load_sessions();
453 457 };
454 458
455 459 var exists = false;
456 460 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
457 461 if ($(v).data('name') === filename) { exists = true; return false; }
458 462 });
459 463
460 464 if (exists) {
461 465 dialog.modal({
462 466 title : "Replace file",
463 467 body : 'There is already a file named ' + filename + ', do you want to replace it?',
464 468 buttons : {
465 469 Overwrite : {
466 470 class: "btn-danger",
467 471 click: function () {
468 472 that.contents.save(path, model).then(on_success);
469 473 }
470 474 },
471 475 Cancel : {
472 476 click: function() { item.remove(); }
473 477 }
474 478 }
475 479 });
476 480 } else {
477 481 that.contents.save(path, model).then(on_success);
478 482 }
479 483
480 484 return false;
481 485 });
482 486 var cancel_button = $('<button/>').text("Cancel")
483 487 .addClass("btn btn-default btn-xs")
484 488 .click(function (e) {
485 489 item.remove();
486 490 return false;
487 491 });
488 492 item.find(".item_buttons").empty()
489 493 .append(upload_button)
490 494 .append(cancel_button);
491 495 };
492 496
493 497
494 498 // Backwards compatability.
495 499 IPython.NotebookList = NotebookList;
496 500
497 501 return {'NotebookList': NotebookList};
498 502 });
General Comments 0
You need to be logged in to leave comments. Login now