##// END OF EJS Templates
Make sure every function appears in the Notebook class.
Jonathan Frederic -
Show More
@@ -1,2387 +1,2400 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 /**
5 5 * @module notebook
6 6 */
7 7 define([
8 8 'base/js/namespace',
9 9 'jquery',
10 10 'base/js/utils',
11 11 'base/js/dialog',
12 12 'notebook/js/cell',
13 13 'notebook/js/textcell',
14 14 'notebook/js/codecell',
15 15 'services/sessions/session',
16 16 'notebook/js/celltoolbar',
17 17 'components/marked/lib/marked',
18 18 'codemirror/lib/codemirror',
19 19 'codemirror/addon/runmode/runmode',
20 20 'notebook/js/mathjaxutils',
21 21 'base/js/keyboard',
22 22 'notebook/js/tooltip',
23 23 'notebook/js/celltoolbarpresets/default',
24 24 'notebook/js/celltoolbarpresets/rawcell',
25 25 'notebook/js/celltoolbarpresets/slideshow',
26 26 'notebook/js/scrollmanager'
27 27 ], function (
28 28 IPython,
29 29 $,
30 30 utils,
31 31 dialog,
32 32 cellmod,
33 33 textcell,
34 34 codecell,
35 35 session,
36 36 celltoolbar,
37 37 marked,
38 38 CodeMirror,
39 39 runMode,
40 40 mathjaxutils,
41 41 keyboard,
42 42 tooltip,
43 43 default_celltoolbar,
44 44 rawcell_celltoolbar,
45 45 slideshow_celltoolbar,
46 46 scrollmanager
47 47 ) {
48 48 "use strict";
49 49
50 50 /**
51 51 * Contains and manages cells.
52 52 * @class Notebook
53 53 * @param {String} selector
54 54 * @param {dictionary} options - Dictionary of keyword arguments.
55 55 * events: $(Events) instance
56 56 * keyboard_manager: KeyboardManager instance
57 57 * contents: Contents instance
58 58 * save_widget: SaveWidget instance
59 59 * config: dictionary
60 60 * base_url : string
61 61 * notebook_path : string
62 62 * notebook_name : string
63 63 */
64 64 var Notebook = function (selector, options) {
65 65 this.config = utils.mergeopt(Notebook, options.config);
66 66 this.base_url = options.base_url;
67 67 this.notebook_path = options.notebook_path;
68 68 this.notebook_name = options.notebook_name;
69 69 this.events = options.events;
70 70 this.keyboard_manager = options.keyboard_manager;
71 71 this.contents = options.contents;
72 72 this.save_widget = options.save_widget;
73 73 this.tooltip = new tooltip.Tooltip(this.events);
74 74 this.ws_url = options.ws_url;
75 75 this._session_starting = false;
76 76 this.default_cell_type = this.config.default_cell_type || 'code';
77 77
78 78 // Create default scroll manager.
79 79 this.scroll_manager = new scrollmanager.ScrollManager(this);
80 80
81 81 // TODO: This code smells (and the other `= this` line a couple lines down)
82 82 // We need a better way to deal with circular instance references.
83 83 this.keyboard_manager.notebook = this;
84 84 this.save_widget.notebook = this;
85 85
86 86 mathjaxutils.init();
87 87
88 88 if (marked) {
89 89 marked.setOptions({
90 90 gfm : true,
91 91 tables: true,
92 92 // FIXME: probably want central config for CodeMirror theme when we have js config
93 93 langPrefix: "cm-s-ipython language-",
94 94 highlight: function(code, lang, callback) {
95 95 if (!lang) {
96 96 // no language, no highlight
97 97 if (callback) {
98 98 callback(null, code);
99 99 return;
100 100 } else {
101 101 return code;
102 102 }
103 103 }
104 104 utils.requireCodeMirrorMode(lang, function (spec) {
105 105 var el = document.createElement("div");
106 106 var mode = CodeMirror.getMode({}, spec);
107 107 if (!mode) {
108 108 console.log("No CodeMirror mode: " + lang);
109 109 callback(null, code);
110 110 return;
111 111 }
112 112 try {
113 113 CodeMirror.runMode(code, spec, el);
114 114 callback(null, el.innerHTML);
115 115 } catch (err) {
116 116 console.log("Failed to highlight " + lang + " code", err);
117 117 callback(err, code);
118 118 }
119 119 }, function (err) {
120 120 console.log("No CodeMirror mode: " + lang);
121 121 callback(err, code);
122 122 });
123 123 }
124 124 });
125 125 }
126 126
127 127 this.element = $(selector);
128 128 this.element.scroll();
129 129 this.element.data("notebook", this);
130 130 this.next_prompt_number = 1;
131 131 this.session = null;
132 132 this.kernel = null;
133 133 this.clipboard = null;
134 134 this.undelete_backup = null;
135 135 this.undelete_index = null;
136 136 this.undelete_below = false;
137 137 this.paste_enabled = false;
138 138 this.writable = false;
139 139 // It is important to start out in command mode to match the intial mode
140 140 // of the KeyboardManager.
141 141 this.mode = 'command';
142 142 this.set_dirty(false);
143 143 this.metadata = {};
144 144 this._checkpoint_after_save = false;
145 145 this.last_checkpoint = null;
146 146 this.checkpoints = [];
147 147 this.autosave_interval = 0;
148 148 this.autosave_timer = null;
149 149 // autosave *at most* every two minutes
150 150 this.minimum_autosave_interval = 120000;
151 151 this.notebook_name_blacklist_re = /[\/\\:]/;
152 152 this.nbformat = 4; // Increment this when changing the nbformat
153 153 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
154 154 this.codemirror_mode = 'ipython';
155 155 this.create_elements();
156 156 this.bind_events();
157 157 this.kernel_selector = null;
158 158 this.dirty = null;
159 159 this.trusted = null;
160 160 this._fully_loaded = false;
161 161
162 162 // Trigger cell toolbar registration.
163 163 default_celltoolbar.register(this);
164 164 rawcell_celltoolbar.register(this);
165 165 slideshow_celltoolbar.register(this);
166 166
167 167 // prevent assign to miss-typed properties.
168 168 Object.seal(this);
169 169 };
170 170
171 171 Notebook.options_default = {
172 172 // can be any cell type, or the special values of
173 173 // 'above', 'below', or 'selected' to get the value from another cell.
174 174 Notebook: {
175 175 default_cell_type: 'code'
176 176 }
177 177 };
178 178
179 179 /**
180 180 * Create an HTML and CSS representation of the notebook.
181 181 */
182 182 Notebook.prototype.create_elements = function () {
183 183 var that = this;
184 184 this.element.attr('tabindex','-1');
185 185 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
186 186 // We add this end_space div to the end of the notebook div to:
187 187 // i) provide a margin between the last cell and the end of the notebook
188 188 // ii) to prevent the div from scrolling up when the last cell is being
189 189 // edited, but is too low on the page, which browsers will do automatically.
190 190 var end_space = $('<div/>').addClass('end_space');
191 191 end_space.dblclick(function (e) {
192 192 var ncells = that.ncells();
193 193 that.insert_cell_below('code',ncells-1);
194 194 });
195 195 this.element.append(this.container);
196 196 this.container.append(end_space);
197 197 };
198 198
199 199 /**
200 200 * Bind JavaScript events: key presses and custom IPython events.
201 201 */
202 202 Notebook.prototype.bind_events = function () {
203 203 var that = this;
204 204
205 205 this.events.on('set_next_input.Notebook', function (event, data) {
206 206 if (data.replace) {
207 207 data.cell.set_text(data.text);
208 208 data.cell.clear_output();
209 209 } else {
210 210 var index = that.find_cell_index(data.cell);
211 211 var new_cell = that.insert_cell_below('code',index);
212 212 new_cell.set_text(data.text);
213 213 }
214 214 that.dirty = true;
215 215 });
216 216
217 217 this.events.on('unrecognized_cell.Cell', function () {
218 218 that.warn_nbformat_minor();
219 219 });
220 220
221 221 this.events.on('unrecognized_output.OutputArea', function () {
222 222 that.warn_nbformat_minor();
223 223 });
224 224
225 225 this.events.on('set_dirty.Notebook', function (event, data) {
226 226 that.dirty = data.value;
227 227 });
228 228
229 229 this.events.on('trust_changed.Notebook', function (event, trusted) {
230 230 that.trusted = trusted;
231 231 });
232 232
233 233 this.events.on('select.Cell', function (event, data) {
234 234 var index = that.find_cell_index(data.cell);
235 235 that.select(index);
236 236 });
237 237
238 238 this.events.on('edit_mode.Cell', function (event, data) {
239 239 that.handle_edit_mode(data.cell);
240 240 });
241 241
242 242 this.events.on('command_mode.Cell', function (event, data) {
243 243 that.handle_command_mode(data.cell);
244 244 });
245 245
246 246 this.events.on('spec_changed.Kernel', function(event, data) {
247 247 that.metadata.kernelspec =
248 248 {name: data.name, display_name: data.display_name};
249 249 });
250 250
251 251 this.events.on('kernel_ready.Kernel', function(event, data) {
252 252 var kinfo = data.kernel.info_reply;
253 253 var langinfo = kinfo.language_info || {};
254 254 that.metadata.language_info = langinfo;
255 255 // Mode 'null' should be plain, unhighlighted text.
256 256 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
257 257 that.set_codemirror_mode(cm_mode);
258 258 });
259 259
260 260 var collapse_time = function (time) {
261 261 var app_height = $('#ipython-main-app').height(); // content height
262 262 var splitter_height = $('div#pager_splitter').outerHeight(true);
263 263 var new_height = app_height - splitter_height;
264 264 that.element.animate({height : new_height + 'px'}, time);
265 265 };
266 266
267 267 this.element.bind('collapse_pager', function (event, extrap) {
268 268 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
269 269 collapse_time(time);
270 270 });
271 271
272 272 var expand_time = function (time) {
273 273 var app_height = $('#ipython-main-app').height(); // content height
274 274 var splitter_height = $('div#pager_splitter').outerHeight(true);
275 275 var pager_height = $('div#pager').outerHeight(true);
276 276 var new_height = app_height - pager_height - splitter_height;
277 277 that.element.animate({height : new_height + 'px'}, time);
278 278 };
279 279
280 280 this.element.bind('expand_pager', function (event, extrap) {
281 281 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
282 282 expand_time(time);
283 283 });
284 284
285 285 // Firefox 22 broke $(window).on("beforeunload")
286 286 // I'm not sure why or how.
287 287 window.onbeforeunload = function (e) {
288 288 // TODO: Make killing the kernel configurable.
289 289 var kill_kernel = false;
290 290 if (kill_kernel) {
291 291 that.session.delete();
292 292 }
293 293 // if we are autosaving, trigger an autosave on nav-away.
294 294 // still warn, because if we don't the autosave may fail.
295 295 if (that.dirty) {
296 296 if ( that.autosave_interval ) {
297 297 // schedule autosave in a timeout
298 298 // this gives you a chance to forcefully discard changes
299 299 // by reloading the page if you *really* want to.
300 300 // the timer doesn't start until you *dismiss* the dialog.
301 301 setTimeout(function () {
302 302 if (that.dirty) {
303 303 that.save_notebook();
304 304 }
305 305 }, 1000);
306 306 return "Autosave in progress, latest changes may be lost.";
307 307 } else {
308 308 return "Unsaved changes will be lost.";
309 309 }
310 310 }
311 311 // Null is the *only* return value that will make the browser not
312 312 // pop up the "don't leave" dialog.
313 313 return null;
314 314 };
315 315 };
316 316
317 /**
318 * trigger a warning dialog about missing functionality from newer minor versions
319 */
317 320 Notebook.prototype.warn_nbformat_minor = function (event) {
318 /**
319 * trigger a warning dialog about missing functionality from newer minor versions
320 */
321 321 var v = 'v' + this.nbformat + '.';
322 322 var orig_vs = v + this.nbformat_minor;
323 323 var this_vs = v + this.current_nbformat_minor;
324 324 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
325 325 this_vs + ". You can still work with this notebook, but cell and output types " +
326 326 "introduced in later notebook versions will not be available.";
327 327
328 328 dialog.modal({
329 329 notebook: this,
330 330 keyboard_manager: this.keyboard_manager,
331 331 title : "Newer Notebook",
332 332 body : msg,
333 333 buttons : {
334 334 OK : {
335 335 "class" : "btn-danger"
336 336 }
337 337 }
338 338 });
339 339 }
340 340
341 341 /**
342 342 * Set the dirty flag, and trigger the set_dirty.Notebook event
343 343 */
344 344 Notebook.prototype.set_dirty = function (value) {
345 345 if (value === undefined) {
346 346 value = true;
347 347 }
348 348 if (this.dirty == value) {
349 349 return;
350 350 }
351 351 this.events.trigger('set_dirty.Notebook', {value: value});
352 352 };
353 353
354 354 /**
355 355 * Scroll the top of the page to a given cell.
356 356 *
357 357 * @param {Number} cell_number An index of the cell to view
358 358 * @param {Number} time Animation time in milliseconds
359 359 * @return {Number} Pixel offset from the top of the container
360 360 */
361 361 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
362 362 var cells = this.get_cells();
363 363 time = time || 0;
364 364 cell_number = Math.min(cells.length-1,cell_number);
365 365 cell_number = Math.max(0 ,cell_number);
366 366 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
367 367 this.element.animate({scrollTop:scroll_value}, time);
368 368 return scroll_value;
369 369 };
370 370
371 371 /**
372 372 * Scroll to the bottom of the page.
373 373 */
374 374 Notebook.prototype.scroll_to_bottom = function () {
375 375 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
376 376 };
377 377
378 378 /**
379 379 * Scroll to the top of the page.
380 380 */
381 381 Notebook.prototype.scroll_to_top = function () {
382 382 this.element.animate({scrollTop:0}, 0);
383 383 };
384 384
385 385 // Edit Notebook metadata
386 386
387 387 Notebook.prototype.edit_metadata = function () {
388 388 var that = this;
389 389 dialog.edit_metadata({
390 390 md: this.metadata,
391 391 callback: function (md) {
392 392 that.metadata = md;
393 393 },
394 394 name: 'Notebook',
395 395 notebook: this,
396 396 keyboard_manager: this.keyboard_manager});
397 397 };
398 398
399 399 // Cell indexing, retrieval, etc.
400 400
401 401 /**
402 402 * Get all cell elements in the notebook.
403 403 *
404 404 * @return {jQuery} A selector of all cell elements
405 405 */
406 406 Notebook.prototype.get_cell_elements = function () {
407 407 return this.container.find(".cell").not('.cell .cell');
408 408 };
409 409
410 410 /**
411 411 * Get a particular cell element.
412 412 *
413 413 * @param {Number} index An index of a cell to select
414 414 * @return {jQuery} A selector of the given cell.
415 415 */
416 416 Notebook.prototype.get_cell_element = function (index) {
417 417 var result = null;
418 418 var e = this.get_cell_elements().eq(index);
419 419 if (e.length !== 0) {
420 420 result = e;
421 421 }
422 422 return result;
423 423 };
424 424
425 425 /**
426 426 * Try to get a particular cell by msg_id.
427 427 *
428 428 * @param {String} msg_id A message UUID
429 429 * @return {Cell} Cell or null if no cell was found.
430 430 */
431 431 Notebook.prototype.get_msg_cell = function (msg_id) {
432 432 return codecell.CodeCell.msg_cells[msg_id] || null;
433 433 };
434 434
435 435 /**
436 436 * Count the cells in this notebook.
437 437 *
438 438 * @return {Number} The number of cells in this notebook
439 439 */
440 440 Notebook.prototype.ncells = function () {
441 441 return this.get_cell_elements().length;
442 442 };
443 443
444 444 /**
445 445 * Get all Cell objects in this notebook.
446 446 *
447 447 * @return {Array} This notebook's Cell objects
448 448 */
449 // TODO: we are often calling cells as cells()[i], which we should optimize
450 // to cells(i) or a new method.
451 449 Notebook.prototype.get_cells = function () {
450 // TODO: we are often calling cells as cells()[i], which we should optimize
451 // to cells(i) or a new method.
452 452 return this.get_cell_elements().toArray().map(function (e) {
453 453 return $(e).data("cell");
454 454 });
455 455 };
456 456
457 457 /**
458 458 * Get a Cell object from this notebook.
459 459 *
460 460 * @param {Number} index An index of a cell to retrieve
461 461 * @return {Cell} Cell or null if no cell was found.
462 462 */
463 463 Notebook.prototype.get_cell = function (index) {
464 464 var result = null;
465 465 var ce = this.get_cell_element(index);
466 466 if (ce !== null) {
467 467 result = ce.data('cell');
468 468 }
469 469 return result;
470 470 };
471 471
472 472 /**
473 473 * Get the cell below a given cell.
474 474 *
475 475 * @param {Cell} cell The provided cell
476 476 * @return {Cell} the next cell or null if no cell was found.
477 477 */
478 478 Notebook.prototype.get_next_cell = function (cell) {
479 479 var result = null;
480 480 var index = this.find_cell_index(cell);
481 481 if (this.is_valid_cell_index(index+1)) {
482 482 result = this.get_cell(index+1);
483 483 }
484 484 return result;
485 485 };
486 486
487 487 /**
488 488 * Get the cell above a given cell.
489 489 *
490 490 * @param {Cell} cell The provided cell
491 491 * @return {Cell} The previous cell or null if no cell was found.
492 492 */
493 493 Notebook.prototype.get_prev_cell = function (cell) {
494 494 var result = null;
495 495 var index = this.find_cell_index(cell);
496 496 if (index !== null && index > 0) {
497 497 result = this.get_cell(index-1);
498 498 }
499 499 return result;
500 500 };
501 501
502 502 /**
503 503 * Get the numeric index of a given cell.
504 504 *
505 505 * @param {Cell} cell The provided cell
506 506 * @return {Number} The cell's numeric index or null if no cell was found.
507 507 */
508 508 Notebook.prototype.find_cell_index = function (cell) {
509 509 var result = null;
510 510 this.get_cell_elements().filter(function (index) {
511 511 if ($(this).data("cell") === cell) {
512 512 result = index;
513 513 }
514 514 });
515 515 return result;
516 516 };
517 517
518 518 /**
519 519 * Get a given index , or the selected index if none is provided.
520 520 *
521 521 * @param {Number} index A cell's index
522 522 * @return {Number} The given index, or selected index if none is provided.
523 523 */
524 524 Notebook.prototype.index_or_selected = function (index) {
525 525 var i;
526 526 if (index === undefined || index === null) {
527 527 i = this.get_selected_index();
528 528 if (i === null) {
529 529 i = 0;
530 530 }
531 531 } else {
532 532 i = index;
533 533 }
534 534 return i;
535 535 };
536 536
537 537 /**
538 538 * Get the currently selected cell.
539 539 * @return {Cell} The selected cell
540 540 */
541 541 Notebook.prototype.get_selected_cell = function () {
542 542 var index = this.get_selected_index();
543 543 return this.get_cell(index);
544 544 };
545 545
546 546 /**
547 547 * Check whether a cell index is valid.
548 548 *
549 549 * @param {Number} index A cell index
550 550 * @return True if the index is valid, false otherwise
551 551 */
552 552 Notebook.prototype.is_valid_cell_index = function (index) {
553 553 if (index !== null && index >= 0 && index < this.ncells()) {
554 554 return true;
555 555 } else {
556 556 return false;
557 557 }
558 558 };
559 559
560 560 /**
561 561 * Get the index of the currently selected cell.
562 562
563 563 * @return {Number} The selected cell's numeric index
564 564 */
565 565 Notebook.prototype.get_selected_index = function () {
566 566 var result = null;
567 567 this.get_cell_elements().filter(function (index) {
568 568 if ($(this).data("cell").selected === true) {
569 569 result = index;
570 570 }
571 571 });
572 572 return result;
573 573 };
574 574
575 575
576 576 // Cell selection.
577 577
578 578 /**
579 579 * Programmatically select a cell.
580 580 *
581 581 * @param {Number} index A cell's index
582 582 * @return {Notebook} This notebook
583 583 */
584 584 Notebook.prototype.select = function (index) {
585 585 if (this.is_valid_cell_index(index)) {
586 586 var sindex = this.get_selected_index();
587 587 if (sindex !== null && index !== sindex) {
588 588 // If we are about to select a different cell, make sure we are
589 589 // first in command mode.
590 590 if (this.mode !== 'command') {
591 591 this.command_mode();
592 592 }
593 593 this.get_cell(sindex).unselect();
594 594 }
595 595 var cell = this.get_cell(index);
596 596 cell.select();
597 597 if (cell.cell_type === 'heading') {
598 598 this.events.trigger('selected_cell_type_changed.Notebook',
599 599 {'cell_type':cell.cell_type,level:cell.level}
600 600 );
601 601 } else {
602 602 this.events.trigger('selected_cell_type_changed.Notebook',
603 603 {'cell_type':cell.cell_type}
604 604 );
605 605 }
606 606 }
607 607 return this;
608 608 };
609 609
610 610 /**
611 611 * Programmatically select the next cell.
612 612 *
613 613 * @return {Notebook} This notebook
614 614 */
615 615 Notebook.prototype.select_next = function () {
616 616 var index = this.get_selected_index();
617 617 this.select(index+1);
618 618 return this;
619 619 };
620 620
621 621 /**
622 622 * Programmatically select the previous cell.
623 623 *
624 624 * @return {Notebook} This notebook
625 625 */
626 626 Notebook.prototype.select_prev = function () {
627 627 var index = this.get_selected_index();
628 628 this.select(index-1);
629 629 return this;
630 630 };
631 631
632 632
633 633 // Edit/Command mode
634 634
635 635 /**
636 636 * Gets the index of the cell that is in edit mode.
637 637 *
638 638 * @return index {int}
639 639 **/
640 640 Notebook.prototype.get_edit_index = function () {
641 641 var result = null;
642 642 this.get_cell_elements().filter(function (index) {
643 643 if ($(this).data("cell").mode === 'edit') {
644 644 result = index;
645 645 }
646 646 });
647 647 return result;
648 648 };
649 649
650 650 /**
651 651 * Handle when a a cell blurs and the notebook should enter command mode.
652 652 *
653 653 * @param [cell] {Cell} Cell to enter command mode on.
654 654 **/
655 655 Notebook.prototype.handle_command_mode = function (cell) {
656 656 if (this.mode !== 'command') {
657 657 cell.command_mode();
658 658 this.mode = 'command';
659 659 this.events.trigger('command_mode.Notebook');
660 660 this.keyboard_manager.command_mode();
661 661 }
662 662 };
663 663
664 664 /**
665 665 * Make the notebook enter command mode.
666 666 **/
667 667 Notebook.prototype.command_mode = function () {
668 668 var cell = this.get_cell(this.get_edit_index());
669 669 if (cell && this.mode !== 'command') {
670 670 // We don't call cell.command_mode, but rather call cell.focus_cell()
671 671 // which will blur and CM editor and trigger the call to
672 672 // handle_command_mode.
673 673 cell.focus_cell();
674 674 }
675 675 };
676 676
677 677 /**
678 678 * Handle when a cell fires it's edit_mode event.
679 679 *
680 680 * @param [cell] {Cell} Cell to enter edit mode on.
681 681 **/
682 682 Notebook.prototype.handle_edit_mode = function (cell) {
683 683 if (cell && this.mode !== 'edit') {
684 684 cell.edit_mode();
685 685 this.mode = 'edit';
686 686 this.events.trigger('edit_mode.Notebook');
687 687 this.keyboard_manager.edit_mode();
688 688 }
689 689 };
690 690
691 691 /**
692 692 * Make a cell enter edit mode.
693 693 **/
694 694 Notebook.prototype.edit_mode = function () {
695 695 var cell = this.get_selected_cell();
696 696 if (cell && this.mode !== 'edit') {
697 697 cell.unrender();
698 698 cell.focus_editor();
699 699 }
700 700 };
701 701
702 702 /**
703 703 * Focus the currently selected cell.
704 704 **/
705 705 Notebook.prototype.focus_cell = function () {
706 706 var cell = this.get_selected_cell();
707 707 if (cell === null) {return;} // No cell is selected
708 708 cell.focus_cell();
709 709 };
710 710
711 711 // Cell movement
712 712
713 713 /**
714 714 * Move given (or selected) cell up and select it.
715 715 *
716 716 * @param [index] {integer} cell index
717 717 * @return {Notebook} This notebook
718 718 **/
719 719 Notebook.prototype.move_cell_up = function (index) {
720 720 var i = this.index_or_selected(index);
721 721 if (this.is_valid_cell_index(i) && i > 0) {
722 722 var pivot = this.get_cell_element(i-1);
723 723 var tomove = this.get_cell_element(i);
724 724 if (pivot !== null && tomove !== null) {
725 725 tomove.detach();
726 726 pivot.before(tomove);
727 727 this.select(i-1);
728 728 var cell = this.get_selected_cell();
729 729 cell.focus_cell();
730 730 }
731 731 this.set_dirty(true);
732 732 }
733 733 return this;
734 734 };
735 735
736 736
737 737 /**
738 738 * Move given (or selected) cell down and select it
739 739 *
740 740 * @param [index] {integer} cell index
741 741 * @return {Notebook} This notebook
742 742 **/
743 743 Notebook.prototype.move_cell_down = function (index) {
744 744 var i = this.index_or_selected(index);
745 745 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
746 746 var pivot = this.get_cell_element(i+1);
747 747 var tomove = this.get_cell_element(i);
748 748 if (pivot !== null && tomove !== null) {
749 749 tomove.detach();
750 750 pivot.after(tomove);
751 751 this.select(i+1);
752 752 var cell = this.get_selected_cell();
753 753 cell.focus_cell();
754 754 }
755 755 }
756 756 this.set_dirty();
757 757 return this;
758 758 };
759 759
760 760
761 761 // Insertion, deletion.
762 762
763 763 /**
764 764 * Delete a cell from the notebook.
765 765 *
766 766 * @param [index] A cell's numeric index
767 767 * @return {Notebook} This notebook
768 768 */
769 769 Notebook.prototype.delete_cell = function (index) {
770 770 var i = this.index_or_selected(index);
771 771 var cell = this.get_cell(i);
772 772 if (!cell.is_deletable()) {
773 773 return this;
774 774 }
775 775
776 776 this.undelete_backup = cell.toJSON();
777 777 $('#undelete_cell').removeClass('disabled');
778 778 if (this.is_valid_cell_index(i)) {
779 779 var old_ncells = this.ncells();
780 780 var ce = this.get_cell_element(i);
781 781 ce.remove();
782 782 if (i === 0) {
783 783 // Always make sure we have at least one cell.
784 784 if (old_ncells === 1) {
785 785 this.insert_cell_below('code');
786 786 }
787 787 this.select(0);
788 788 this.undelete_index = 0;
789 789 this.undelete_below = false;
790 790 } else if (i === old_ncells-1 && i !== 0) {
791 791 this.select(i-1);
792 792 this.undelete_index = i - 1;
793 793 this.undelete_below = true;
794 794 } else {
795 795 this.select(i);
796 796 this.undelete_index = i;
797 797 this.undelete_below = false;
798 798 }
799 799 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
800 800 this.set_dirty(true);
801 801 }
802 802 return this;
803 803 };
804 804
805 805 /**
806 806 * Restore the most recently deleted cell.
807 807 */
808 808 Notebook.prototype.undelete_cell = function() {
809 809 if (this.undelete_backup !== null && this.undelete_index !== null) {
810 810 var current_index = this.get_selected_index();
811 811 if (this.undelete_index < current_index) {
812 812 current_index = current_index + 1;
813 813 }
814 814 if (this.undelete_index >= this.ncells()) {
815 815 this.select(this.ncells() - 1);
816 816 }
817 817 else {
818 818 this.select(this.undelete_index);
819 819 }
820 820 var cell_data = this.undelete_backup;
821 821 var new_cell = null;
822 822 if (this.undelete_below) {
823 823 new_cell = this.insert_cell_below(cell_data.cell_type);
824 824 } else {
825 825 new_cell = this.insert_cell_above(cell_data.cell_type);
826 826 }
827 827 new_cell.fromJSON(cell_data);
828 828 if (this.undelete_below) {
829 829 this.select(current_index+1);
830 830 } else {
831 831 this.select(current_index);
832 832 }
833 833 this.undelete_backup = null;
834 834 this.undelete_index = null;
835 835 }
836 836 $('#undelete_cell').addClass('disabled');
837 837 };
838 838
839 839 /**
840 840 * Insert a cell so that after insertion the cell is at given index.
841 841 *
842 842 * If cell type is not provided, it will default to the type of the
843 843 * currently active cell.
844 844 *
845 845 * Similar to insert_above, but index parameter is mandatory
846 846 *
847 847 * Index will be brought back into the accessible range [0,n]
848 848 *
849 849 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
850 850 * @param [index] {int} a valid index where to insert cell
851 851 *
852 852 * @return cell {cell|null} created cell or null
853 853 **/
854 854 Notebook.prototype.insert_cell_at_index = function(type, index){
855 855
856 856 var ncells = this.ncells();
857 857 index = Math.min(index, ncells);
858 858 index = Math.max(index, 0);
859 859 var cell = null;
860 860 type = type || this.default_cell_type;
861 861 if (type === 'above') {
862 862 if (index > 0) {
863 863 type = this.get_cell(index-1).cell_type;
864 864 } else {
865 865 type = 'code';
866 866 }
867 867 } else if (type === 'below') {
868 868 if (index < ncells) {
869 869 type = this.get_cell(index).cell_type;
870 870 } else {
871 871 type = 'code';
872 872 }
873 873 } else if (type === 'selected') {
874 874 type = this.get_selected_cell().cell_type;
875 875 }
876 876
877 877 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
878 878 var cell_options = {
879 879 events: this.events,
880 880 config: this.config,
881 881 keyboard_manager: this.keyboard_manager,
882 882 notebook: this,
883 883 tooltip: this.tooltip
884 884 };
885 885 switch(type) {
886 886 case 'code':
887 887 cell = new codecell.CodeCell(this.kernel, cell_options);
888 888 cell.set_input_prompt();
889 889 break;
890 890 case 'markdown':
891 891 cell = new textcell.MarkdownCell(cell_options);
892 892 break;
893 893 case 'raw':
894 894 cell = new textcell.RawCell(cell_options);
895 895 break;
896 896 default:
897 897 console.log("Unrecognized cell type: ", type, cellmod);
898 898 cell = new cellmod.UnrecognizedCell(cell_options);
899 899 }
900 900
901 901 if(this._insert_element_at_index(cell.element,index)) {
902 902 cell.render();
903 903 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
904 904 cell.refresh();
905 905 // We used to select the cell after we refresh it, but there
906 906 // are now cases were this method is called where select is
907 907 // not appropriate. The selection logic should be handled by the
908 908 // caller of the the top level insert_cell methods.
909 909 this.set_dirty(true);
910 910 }
911 911 }
912 912 return cell;
913 913
914 914 };
915 915
916 916 /**
917 917 * Insert an element at given cell index.
918 918 * return true if everything whent fine.
919 919 *
920 920 * @param element {dom_element} a cell element
921 921 * @param [index] {int} a valid index where to inser cell
922 922 * @private
923 923 **/
924 924 Notebook.prototype._insert_element_at_index = function(element, index){
925 925 if (element === undefined){
926 926 return false;
927 927 }
928 928
929 929 var ncells = this.ncells();
930 930
931 931 if (ncells === 0) {
932 932 // special case append if empty
933 933 this.element.find('div.end_space').before(element);
934 934 } else if ( ncells === index ) {
935 935 // special case append it the end, but not empty
936 936 this.get_cell_element(index-1).after(element);
937 937 } else if (this.is_valid_cell_index(index)) {
938 938 // otherwise always somewhere to append to
939 939 this.get_cell_element(index).before(element);
940 940 } else {
941 941 return false;
942 942 }
943 943
944 944 if (this.undelete_index !== null && index <= this.undelete_index) {
945 945 this.undelete_index = this.undelete_index + 1;
946 946 this.set_dirty(true);
947 947 }
948 948 return true;
949 949 };
950 950
951 951 /**
952 952 * Insert a cell of given type above given index, or at top
953 953 * of notebook if index smaller than 0.
954 954 *
955 955 * default index value is the one of currently selected cell
956 956 *
957 957 * @param [type] {string} cell type
958 958 * @param [index] {integer}
959 959 *
960 960 * @return handle to created cell or null
961 961 **/
962 962 Notebook.prototype.insert_cell_above = function (type, index) {
963 963 index = this.index_or_selected(index);
964 964 return this.insert_cell_at_index(type, index);
965 965 };
966 966
967 967 /**
968 968 * Insert a cell of given type below given index, or at bottom
969 969 * of notebook if index greater than number of cells
970 970 *
971 971 * default index value is the one of currently selected cell
972 972 *
973 973 * @param [type] {string} cell type
974 974 * @param [index] {integer}
975 975 *
976 976 * @return handle to created cell or null
977 977 **/
978 978 Notebook.prototype.insert_cell_below = function (type, index) {
979 979 index = this.index_or_selected(index);
980 980 return this.insert_cell_at_index(type, index+1);
981 981 };
982 982
983 983
984 984 /**
985 985 * Insert cell at end of notebook
986 986 *
987 987 * @param {String} type cell type
988 988 *
989 989 * @return the added cell; or null
990 990 **/
991 991 Notebook.prototype.insert_cell_at_bottom = function (type){
992 992 var len = this.ncells();
993 993 return this.insert_cell_below(type,len-1);
994 994 };
995 995
996 996 /**
997 997 * Turn a cell into a code cell.
998 998 *
999 999 * @param {Number} [index] A cell's index
1000 1000 */
1001 1001 Notebook.prototype.to_code = function (index) {
1002 1002 var i = this.index_or_selected(index);
1003 1003 if (this.is_valid_cell_index(i)) {
1004 1004 var source_cell = this.get_cell(i);
1005 1005 if (!(source_cell instanceof codecell.CodeCell)) {
1006 1006 var target_cell = this.insert_cell_below('code',i);
1007 1007 var text = source_cell.get_text();
1008 1008 if (text === source_cell.placeholder) {
1009 1009 text = '';
1010 1010 }
1011 1011 //metadata
1012 1012 target_cell.metadata = source_cell.metadata;
1013 1013
1014 1014 target_cell.set_text(text);
1015 1015 // make this value the starting point, so that we can only undo
1016 1016 // to this state, instead of a blank cell
1017 1017 target_cell.code_mirror.clearHistory();
1018 1018 source_cell.element.remove();
1019 1019 this.select(i);
1020 1020 var cursor = source_cell.code_mirror.getCursor();
1021 1021 target_cell.code_mirror.setCursor(cursor);
1022 1022 this.set_dirty(true);
1023 1023 }
1024 1024 }
1025 1025 };
1026 1026
1027 1027 /**
1028 1028 * Turn a cell into a Markdown cell.
1029 1029 *
1030 1030 * @param {Number} [index] A cell's index
1031 1031 */
1032 1032 Notebook.prototype.to_markdown = function (index) {
1033 1033 var i = this.index_or_selected(index);
1034 1034 if (this.is_valid_cell_index(i)) {
1035 1035 var source_cell = this.get_cell(i);
1036 1036
1037 1037 if (!(source_cell instanceof textcell.MarkdownCell)) {
1038 1038 var target_cell = this.insert_cell_below('markdown',i);
1039 1039 var text = source_cell.get_text();
1040 1040
1041 1041 if (text === source_cell.placeholder) {
1042 1042 text = '';
1043 1043 }
1044 1044 // metadata
1045 1045 target_cell.metadata = source_cell.metadata;
1046 1046 // We must show the editor before setting its contents
1047 1047 target_cell.unrender();
1048 1048 target_cell.set_text(text);
1049 1049 // make this value the starting point, so that we can only undo
1050 1050 // to this state, instead of a blank cell
1051 1051 target_cell.code_mirror.clearHistory();
1052 1052 source_cell.element.remove();
1053 1053 this.select(i);
1054 1054 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1055 1055 target_cell.render();
1056 1056 }
1057 1057 var cursor = source_cell.code_mirror.getCursor();
1058 1058 target_cell.code_mirror.setCursor(cursor);
1059 1059 this.set_dirty(true);
1060 1060 }
1061 1061 }
1062 1062 };
1063 1063
1064 1064 /**
1065 1065 * Turn a cell into a raw text cell.
1066 1066 *
1067 1067 * @param {Number} [index] A cell's index
1068 1068 */
1069 1069 Notebook.prototype.to_raw = function (index) {
1070 1070 var i = this.index_or_selected(index);
1071 1071 if (this.is_valid_cell_index(i)) {
1072 1072 var target_cell = null;
1073 1073 var source_cell = this.get_cell(i);
1074 1074
1075 1075 if (!(source_cell instanceof textcell.RawCell)) {
1076 1076 target_cell = this.insert_cell_below('raw',i);
1077 1077 var text = source_cell.get_text();
1078 1078 if (text === source_cell.placeholder) {
1079 1079 text = '';
1080 1080 }
1081 1081 //metadata
1082 1082 target_cell.metadata = source_cell.metadata;
1083 1083 // We must show the editor before setting its contents
1084 1084 target_cell.unrender();
1085 1085 target_cell.set_text(text);
1086 1086 // make this value the starting point, so that we can only undo
1087 1087 // to this state, instead of a blank cell
1088 1088 target_cell.code_mirror.clearHistory();
1089 1089 source_cell.element.remove();
1090 1090 this.select(i);
1091 1091 var cursor = source_cell.code_mirror.getCursor();
1092 1092 target_cell.code_mirror.setCursor(cursor);
1093 1093 this.set_dirty(true);
1094 1094 }
1095 1095 }
1096 1096 };
1097 1097
1098 1098 Notebook.prototype._warn_heading = function () {
1099 1099 /**
1100 1100 * warn about heading cells being removed
1101 1101 * @private
1102 1102 */
1103 1103 dialog.modal({
1104 1104 notebook: this,
1105 1105 keyboard_manager: this.keyboard_manager,
1106 1106 title : "Use markdown headings",
1107 1107 body : $("<p/>").text(
1108 1108 'IPython no longer uses special heading cells. ' +
1109 1109 'Instead, write your headings in Markdown cells using # characters:'
1110 1110 ).append($('<pre/>').text(
1111 1111 '## This is a level 2 heading'
1112 1112 )),
1113 1113 buttons : {
1114 1114 "OK" : {}
1115 1115 }
1116 1116 });
1117 1117 };
1118 1118
1119 1119 Notebook.prototype.to_heading = function (index, level) {
1120 1120 /**
1121 1121 * Turn a cell into a markdown cell with a heading.
1122 1122 *
1123 1123 * @param {Number} [index] A cell's index
1124 1124 * @param {Number} [level] A heading level (e.g., 1 for h1)
1125 1125 */
1126 1126 this.to_markdown(index);
1127 1127 level = level || 1;
1128 1128 var i = this.index_or_selected(index);
1129 1129 if (this.is_valid_cell_index(i)) {
1130 1130 var cell = this.get_cell(i);
1131 1131 cell.set_heading_level(level);
1132 1132 this.set_dirty(true);
1133 1133 }
1134 1134 };
1135 1135
1136 1136
1137 1137 // Cut/Copy/Paste
1138 1138
1139 1139 /**
1140 1140 * Enable UI elements for pasting cells.
1141 1141 */
1142 1142 Notebook.prototype.enable_paste = function () {
1143 1143 var that = this;
1144 1144 if (!this.paste_enabled) {
1145 1145 $('#paste_cell_replace').removeClass('disabled')
1146 1146 .on('click', function () {that.paste_cell_replace();});
1147 1147 $('#paste_cell_above').removeClass('disabled')
1148 1148 .on('click', function () {that.paste_cell_above();});
1149 1149 $('#paste_cell_below').removeClass('disabled')
1150 1150 .on('click', function () {that.paste_cell_below();});
1151 1151 this.paste_enabled = true;
1152 1152 }
1153 1153 };
1154 1154
1155 1155 /**
1156 1156 * Disable UI elements for pasting cells.
1157 1157 */
1158 1158 Notebook.prototype.disable_paste = function () {
1159 1159 if (this.paste_enabled) {
1160 1160 $('#paste_cell_replace').addClass('disabled').off('click');
1161 1161 $('#paste_cell_above').addClass('disabled').off('click');
1162 1162 $('#paste_cell_below').addClass('disabled').off('click');
1163 1163 this.paste_enabled = false;
1164 1164 }
1165 1165 };
1166 1166
1167 1167 /**
1168 1168 * Cut a cell.
1169 1169 */
1170 1170 Notebook.prototype.cut_cell = function () {
1171 1171 this.copy_cell();
1172 1172 this.delete_cell();
1173 1173 };
1174 1174
1175 1175 /**
1176 1176 * Copy a cell.
1177 1177 */
1178 1178 Notebook.prototype.copy_cell = function () {
1179 1179 var cell = this.get_selected_cell();
1180 1180 this.clipboard = cell.toJSON();
1181 1181 // remove undeletable status from the copied cell
1182 1182 if (this.clipboard.metadata.deletable !== undefined) {
1183 1183 delete this.clipboard.metadata.deletable;
1184 1184 }
1185 1185 this.enable_paste();
1186 1186 };
1187 1187
1188 1188 /**
1189 1189 * Replace the selected cell with a cell in the clipboard.
1190 1190 */
1191 1191 Notebook.prototype.paste_cell_replace = function () {
1192 1192 if (this.clipboard !== null && this.paste_enabled) {
1193 1193 var cell_data = this.clipboard;
1194 1194 var new_cell = this.insert_cell_above(cell_data.cell_type);
1195 1195 new_cell.fromJSON(cell_data);
1196 1196 var old_cell = this.get_next_cell(new_cell);
1197 1197 this.delete_cell(this.find_cell_index(old_cell));
1198 1198 this.select(this.find_cell_index(new_cell));
1199 1199 }
1200 1200 };
1201 1201
1202 1202 /**
1203 1203 * Paste a cell from the clipboard above the selected cell.
1204 1204 */
1205 1205 Notebook.prototype.paste_cell_above = function () {
1206 1206 if (this.clipboard !== null && this.paste_enabled) {
1207 1207 var cell_data = this.clipboard;
1208 1208 var new_cell = this.insert_cell_above(cell_data.cell_type);
1209 1209 new_cell.fromJSON(cell_data);
1210 1210 new_cell.focus_cell();
1211 1211 }
1212 1212 };
1213 1213
1214 1214 /**
1215 1215 * Paste a cell from the clipboard below the selected cell.
1216 1216 */
1217 1217 Notebook.prototype.paste_cell_below = function () {
1218 1218 if (this.clipboard !== null && this.paste_enabled) {
1219 1219 var cell_data = this.clipboard;
1220 1220 var new_cell = this.insert_cell_below(cell_data.cell_type);
1221 1221 new_cell.fromJSON(cell_data);
1222 1222 new_cell.focus_cell();
1223 1223 }
1224 1224 };
1225 1225
1226 1226 // Split/merge
1227 1227
1228 1228 /**
1229 1229 * Split the selected cell into two, at the cursor.
1230 1230 */
1231 1231 Notebook.prototype.split_cell = function () {
1232 1232 var cell = this.get_selected_cell();
1233 1233 if (cell.is_splittable()) {
1234 1234 var texta = cell.get_pre_cursor();
1235 1235 var textb = cell.get_post_cursor();
1236 1236 cell.set_text(textb);
1237 1237 var new_cell = this.insert_cell_above(cell.cell_type);
1238 1238 // Unrender the new cell so we can call set_text.
1239 1239 new_cell.unrender();
1240 1240 new_cell.set_text(texta);
1241 1241 }
1242 1242 };
1243 1243
1244 1244 /**
1245 1245 * Combine the selected cell into the cell above it.
1246 1246 */
1247 1247 Notebook.prototype.merge_cell_above = function () {
1248 1248 var index = this.get_selected_index();
1249 1249 var cell = this.get_cell(index);
1250 1250 var render = cell.rendered;
1251 1251 if (!cell.is_mergeable()) {
1252 1252 return;
1253 1253 }
1254 1254 if (index > 0) {
1255 1255 var upper_cell = this.get_cell(index-1);
1256 1256 if (!upper_cell.is_mergeable()) {
1257 1257 return;
1258 1258 }
1259 1259 var upper_text = upper_cell.get_text();
1260 1260 var text = cell.get_text();
1261 1261 if (cell instanceof codecell.CodeCell) {
1262 1262 cell.set_text(upper_text+'\n'+text);
1263 1263 } else {
1264 1264 cell.unrender(); // Must unrender before we set_text.
1265 1265 cell.set_text(upper_text+'\n\n'+text);
1266 1266 if (render) {
1267 1267 // The rendered state of the final cell should match
1268 1268 // that of the original selected cell;
1269 1269 cell.render();
1270 1270 }
1271 1271 }
1272 1272 this.delete_cell(index-1);
1273 1273 this.select(this.find_cell_index(cell));
1274 1274 }
1275 1275 };
1276 1276
1277 1277 /**
1278 1278 * Combine the selected cell into the cell below it.
1279 1279 */
1280 1280 Notebook.prototype.merge_cell_below = function () {
1281 1281 var index = this.get_selected_index();
1282 1282 var cell = this.get_cell(index);
1283 1283 var render = cell.rendered;
1284 1284 if (!cell.is_mergeable()) {
1285 1285 return;
1286 1286 }
1287 1287 if (index < this.ncells()-1) {
1288 1288 var lower_cell = this.get_cell(index+1);
1289 1289 if (!lower_cell.is_mergeable()) {
1290 1290 return;
1291 1291 }
1292 1292 var lower_text = lower_cell.get_text();
1293 1293 var text = cell.get_text();
1294 1294 if (cell instanceof codecell.CodeCell) {
1295 1295 cell.set_text(text+'\n'+lower_text);
1296 1296 } else {
1297 1297 cell.unrender(); // Must unrender before we set_text.
1298 1298 cell.set_text(text+'\n\n'+lower_text);
1299 1299 if (render) {
1300 1300 // The rendered state of the final cell should match
1301 1301 // that of the original selected cell;
1302 1302 cell.render();
1303 1303 }
1304 1304 }
1305 1305 this.delete_cell(index+1);
1306 1306 this.select(this.find_cell_index(cell));
1307 1307 }
1308 1308 };
1309 1309
1310 1310
1311 1311 // Cell collapsing and output clearing
1312 1312
1313 1313 /**
1314 1314 * Hide a cell's output.
1315 1315 *
1316 1316 * @param {Number} index A cell's numeric index
1317 1317 */
1318 1318 Notebook.prototype.collapse_output = function (index) {
1319 1319 var i = this.index_or_selected(index);
1320 1320 var cell = this.get_cell(i);
1321 1321 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1322 1322 cell.collapse_output();
1323 1323 this.set_dirty(true);
1324 1324 }
1325 1325 };
1326 1326
1327 1327 /**
1328 1328 * Hide each code cell's output area.
1329 1329 */
1330 1330 Notebook.prototype.collapse_all_output = function () {
1331 1331 this.get_cells().map(function (cell, i) {
1332 1332 if (cell instanceof codecell.CodeCell) {
1333 1333 cell.collapse_output();
1334 1334 }
1335 1335 });
1336 1336 // this should not be set if the `collapse` key is removed from nbformat
1337 1337 this.set_dirty(true);
1338 1338 };
1339 1339
1340 1340 /**
1341 1341 * Show a cell's output.
1342 1342 *
1343 1343 * @param {Number} index A cell's numeric index
1344 1344 */
1345 1345 Notebook.prototype.expand_output = function (index) {
1346 1346 var i = this.index_or_selected(index);
1347 1347 var cell = this.get_cell(i);
1348 1348 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1349 1349 cell.expand_output();
1350 1350 this.set_dirty(true);
1351 1351 }
1352 1352 };
1353 1353
1354 1354 /**
1355 1355 * Expand each code cell's output area, and remove scrollbars.
1356 1356 */
1357 1357 Notebook.prototype.expand_all_output = function () {
1358 1358 this.get_cells().map(function (cell, i) {
1359 1359 if (cell instanceof codecell.CodeCell) {
1360 1360 cell.expand_output();
1361 1361 }
1362 1362 });
1363 1363 // this should not be set if the `collapse` key is removed from nbformat
1364 1364 this.set_dirty(true);
1365 1365 };
1366 1366
1367 1367 /**
1368 1368 * Clear the selected CodeCell's output area.
1369 1369 *
1370 1370 * @param {Number} index A cell's numeric index
1371 1371 */
1372 1372 Notebook.prototype.clear_output = function (index) {
1373 1373 var i = this.index_or_selected(index);
1374 1374 var cell = this.get_cell(i);
1375 1375 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1376 1376 cell.clear_output();
1377 1377 this.set_dirty(true);
1378 1378 }
1379 1379 };
1380 1380
1381 1381 /**
1382 1382 * Clear each code cell's output area.
1383 1383 */
1384 1384 Notebook.prototype.clear_all_output = function () {
1385 1385 this.get_cells().map(function (cell, i) {
1386 1386 if (cell instanceof codecell.CodeCell) {
1387 1387 cell.clear_output();
1388 1388 }
1389 1389 });
1390 1390 this.set_dirty(true);
1391 1391 };
1392 1392
1393 1393 /**
1394 1394 * Scroll the selected CodeCell's output area.
1395 1395 *
1396 1396 * @param {Number} index A cell's numeric index
1397 1397 */
1398 1398 Notebook.prototype.scroll_output = function (index) {
1399 1399 var i = this.index_or_selected(index);
1400 1400 var cell = this.get_cell(i);
1401 1401 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1402 1402 cell.scroll_output();
1403 1403 this.set_dirty(true);
1404 1404 }
1405 1405 };
1406 1406
1407 1407 /**
1408 1408 * Expand each code cell's output area, and add a scrollbar for long output.
1409 1409 */
1410 1410 Notebook.prototype.scroll_all_output = function () {
1411 1411 this.get_cells().map(function (cell, i) {
1412 1412 if (cell instanceof codecell.CodeCell) {
1413 1413 cell.scroll_output();
1414 1414 }
1415 1415 });
1416 1416 // this should not be set if the `collapse` key is removed from nbformat
1417 1417 this.set_dirty(true);
1418 1418 };
1419 1419
1420 1420 /** Toggle whether a cell's output is collapsed or expanded.
1421 1421 *
1422 1422 * @param {Number} index A cell's numeric index
1423 1423 */
1424 1424 Notebook.prototype.toggle_output = function (index) {
1425 1425 var i = this.index_or_selected(index);
1426 1426 var cell = this.get_cell(i);
1427 1427 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1428 1428 cell.toggle_output();
1429 1429 this.set_dirty(true);
1430 1430 }
1431 1431 };
1432 1432
1433 1433 /**
1434 1434 * Hide/show the output of all cells.
1435 1435 */
1436 1436 Notebook.prototype.toggle_all_output = function () {
1437 1437 this.get_cells().map(function (cell, i) {
1438 1438 if (cell instanceof codecell.CodeCell) {
1439 1439 cell.toggle_output();
1440 1440 }
1441 1441 });
1442 1442 // this should not be set if the `collapse` key is removed from nbformat
1443 1443 this.set_dirty(true);
1444 1444 };
1445 1445
1446 1446 /**
1447 1447 * Toggle a scrollbar for long cell outputs.
1448 1448 *
1449 1449 * @param {Number} index A cell's numeric index
1450 1450 */
1451 1451 Notebook.prototype.toggle_output_scroll = function (index) {
1452 1452 var i = this.index_or_selected(index);
1453 1453 var cell = this.get_cell(i);
1454 1454 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1455 1455 cell.toggle_output_scroll();
1456 1456 this.set_dirty(true);
1457 1457 }
1458 1458 };
1459 1459
1460 1460 /**
1461 1461 * Toggle the scrolling of long output on all cells.
1462 1462 */
1463 1463 Notebook.prototype.toggle_all_output_scroll = function () {
1464 1464 this.get_cells().map(function (cell, i) {
1465 1465 if (cell instanceof codecell.CodeCell) {
1466 1466 cell.toggle_output_scroll();
1467 1467 }
1468 1468 });
1469 1469 // this should not be set if the `collapse` key is removed from nbformat
1470 1470 this.set_dirty(true);
1471 1471 };
1472 1472
1473 1473 // Other cell functions: line numbers, ...
1474 1474
1475 1475 /**
1476 1476 * Toggle line numbers in the selected cell's input area.
1477 1477 */
1478 1478 Notebook.prototype.cell_toggle_line_numbers = function() {
1479 1479 this.get_selected_cell().toggle_line_numbers();
1480 1480 };
1481 1481
1482 1482 /**
1483 1483 * Set the codemirror mode for all code cells, including the default for
1484 1484 * new code cells.
1485 1485 */
1486 1486 Notebook.prototype.set_codemirror_mode = function(newmode){
1487 1487 if (newmode === this.codemirror_mode) {
1488 1488 return;
1489 1489 }
1490 1490 this.codemirror_mode = newmode;
1491 1491 codecell.CodeCell.options_default.cm_config.mode = newmode;
1492 1492
1493 1493 var that = this;
1494 1494 utils.requireCodeMirrorMode(newmode, function (spec) {
1495 1495 that.get_cells().map(function(cell, i) {
1496 1496 if (cell.cell_type === 'code'){
1497 1497 cell.code_mirror.setOption('mode', spec);
1498 1498 // This is currently redundant, because cm_config ends up as
1499 1499 // codemirror's own .options object, but I don't want to
1500 1500 // rely on that.
1501 1501 cell.cm_config.mode = spec;
1502 1502 }
1503 1503 });
1504 1504 });
1505 1505 };
1506 1506
1507 1507 // Session related things
1508 1508
1509 1509 /**
1510 1510 * Start a new session and set it on each code cell.
1511 1511 */
1512 1512 Notebook.prototype.start_session = function (kernel_name) {
1513 1513 if (this._session_starting) {
1514 1514 throw new session.SessionAlreadyStarting();
1515 1515 }
1516 1516 this._session_starting = true;
1517 1517
1518 1518 var options = {
1519 1519 base_url: this.base_url,
1520 1520 ws_url: this.ws_url,
1521 1521 notebook_path: this.notebook_path,
1522 1522 notebook_name: this.notebook_name,
1523 1523 kernel_name: kernel_name,
1524 1524 notebook: this
1525 1525 };
1526 1526
1527 1527 var success = $.proxy(this._session_started, this);
1528 1528 var failure = $.proxy(this._session_start_failed, this);
1529 1529
1530 1530 if (this.session !== null) {
1531 1531 this.session.restart(options, success, failure);
1532 1532 } else {
1533 1533 this.session = new session.Session(options);
1534 1534 this.session.start(success, failure);
1535 1535 }
1536 1536 };
1537 1537
1538 1538
1539 1539 /**
1540 1540 * Once a session is started, link the code cells to the kernel and pass the
1541 1541 * comm manager to the widget manager
1542 1542 * @private
1543 1543 */
1544 1544 Notebook.prototype._session_started = function (){
1545 1545 this._session_starting = false;
1546 1546 this.kernel = this.session.kernel;
1547 1547 var ncells = this.ncells();
1548 1548 for (var i=0; i<ncells; i++) {
1549 1549 var cell = this.get_cell(i);
1550 1550 if (cell instanceof codecell.CodeCell) {
1551 1551 cell.set_kernel(this.session.kernel);
1552 1552 }
1553 1553 }
1554 1554 };
1555 1555 /**
1556 1556 * @private
1557 1557 */
1558 1558 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1559 1559 this._session_starting = false;
1560 1560 utils.log_ajax_error(jqxhr, status, error);
1561 1561 };
1562 1562
1563 1563 /**
1564 1564 * Prompt the user to restart the IPython kernel.
1565 1565 */
1566 1566 Notebook.prototype.restart_kernel = function () {
1567 1567 var that = this;
1568 1568 dialog.modal({
1569 1569 notebook: this,
1570 1570 keyboard_manager: this.keyboard_manager,
1571 1571 title : "Restart kernel or continue running?",
1572 1572 body : $("<p/>").text(
1573 1573 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1574 1574 ),
1575 1575 buttons : {
1576 1576 "Continue running" : {},
1577 1577 "Restart" : {
1578 1578 "class" : "btn-danger",
1579 1579 "click" : function() {
1580 1580 that.kernel.restart();
1581 1581 }
1582 1582 }
1583 1583 }
1584 1584 });
1585 1585 };
1586 1586
1587 1587 /**
1588 1588 * Execute or render cell outputs and go into command mode.
1589 1589 */
1590 1590 Notebook.prototype.execute_cell = function () {
1591 1591 // mode = shift, ctrl, alt
1592 1592 var cell = this.get_selected_cell();
1593 1593
1594 1594 cell.execute();
1595 1595 this.command_mode();
1596 1596 this.set_dirty(true);
1597 1597 };
1598 1598
1599 1599 /**
1600 1600 * Execute or render cell outputs and insert a new cell below.
1601 1601 */
1602 1602 Notebook.prototype.execute_cell_and_insert_below = function () {
1603 1603 var cell = this.get_selected_cell();
1604 1604 var cell_index = this.find_cell_index(cell);
1605 1605
1606 1606 cell.execute();
1607 1607
1608 1608 // If we are at the end always insert a new cell and return
1609 1609 if (cell_index === (this.ncells()-1)) {
1610 1610 this.command_mode();
1611 1611 this.insert_cell_below();
1612 1612 this.select(cell_index+1);
1613 1613 this.edit_mode();
1614 1614 this.scroll_to_bottom();
1615 1615 this.set_dirty(true);
1616 1616 return;
1617 1617 }
1618 1618
1619 1619 this.command_mode();
1620 1620 this.insert_cell_below();
1621 1621 this.select(cell_index+1);
1622 1622 this.edit_mode();
1623 1623 this.set_dirty(true);
1624 1624 };
1625 1625
1626 1626 /**
1627 1627 * Execute or render cell outputs and select the next cell.
1628 1628 */
1629 1629 Notebook.prototype.execute_cell_and_select_below = function () {
1630 1630
1631 1631 var cell = this.get_selected_cell();
1632 1632 var cell_index = this.find_cell_index(cell);
1633 1633
1634 1634 cell.execute();
1635 1635
1636 1636 // If we are at the end always insert a new cell and return
1637 1637 if (cell_index === (this.ncells()-1)) {
1638 1638 this.command_mode();
1639 1639 this.insert_cell_below();
1640 1640 this.select(cell_index+1);
1641 1641 this.edit_mode();
1642 1642 this.scroll_to_bottom();
1643 1643 this.set_dirty(true);
1644 1644 return;
1645 1645 }
1646 1646
1647 1647 this.command_mode();
1648 1648 this.select(cell_index+1);
1649 1649 this.focus_cell();
1650 1650 this.set_dirty(true);
1651 1651 };
1652 1652
1653 1653 /**
1654 1654 * Execute all cells below the selected cell.
1655 1655 */
1656 1656 Notebook.prototype.execute_cells_below = function () {
1657 1657 this.execute_cell_range(this.get_selected_index(), this.ncells());
1658 1658 this.scroll_to_bottom();
1659 1659 };
1660 1660
1661 1661 /**
1662 1662 * Execute all cells above the selected cell.
1663 1663 */
1664 1664 Notebook.prototype.execute_cells_above = function () {
1665 1665 this.execute_cell_range(0, this.get_selected_index());
1666 1666 };
1667 1667
1668 1668 /**
1669 1669 * Execute all cells.
1670 1670 */
1671 1671 Notebook.prototype.execute_all_cells = function () {
1672 1672 this.execute_cell_range(0, this.ncells());
1673 1673 this.scroll_to_bottom();
1674 1674 };
1675 1675
1676 1676 /**
1677 1677 * Execute a contiguous range of cells.
1678 1678 *
1679 1679 * @param {Number} start Index of the first cell to execute (inclusive)
1680 1680 * @param {Number} end Index of the last cell to execute (exclusive)
1681 1681 */
1682 1682 Notebook.prototype.execute_cell_range = function (start, end) {
1683 1683 this.command_mode();
1684 1684 for (var i=start; i<end; i++) {
1685 1685 this.select(i);
1686 1686 this.execute_cell();
1687 1687 }
1688 1688 };
1689 1689
1690 1690 // Persistance and loading
1691 1691
1692 1692 /**
1693 1693 * Getter method for this notebook's name.
1694 1694 *
1695 1695 * @return {String} This notebook's name (excluding file extension)
1696 1696 */
1697 1697 Notebook.prototype.get_notebook_name = function () {
1698 1698 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1699 1699 return nbname;
1700 1700 };
1701 1701
1702 1702 /**
1703 1703 * Setter method for this notebook's name.
1704 1704 *
1705 1705 * @param {String} name A new name for this notebook
1706 1706 */
1707 1707 Notebook.prototype.set_notebook_name = function (name) {
1708 1708 var parent = utils.url_path_split(this.notebook_path)[0];
1709 1709 this.notebook_name = name;
1710 1710 this.notebook_path = utils.url_path_join(parent, name);
1711 1711 };
1712 1712
1713 1713 /**
1714 1714 * Check that a notebook's name is valid.
1715 1715 *
1716 1716 * @param {String} nbname A name for this notebook
1717 1717 * @return {Boolean} True if the name is valid, false if invalid
1718 1718 */
1719 1719 Notebook.prototype.test_notebook_name = function (nbname) {
1720 1720 nbname = nbname || '';
1721 1721 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1722 1722 return true;
1723 1723 } else {
1724 1724 return false;
1725 1725 }
1726 1726 };
1727 1727
1728 1728 /**
1729 1729 * Load a notebook from JSON (.ipynb).
1730 1730 *
1731 1731 * @param {Object} data JSON representation of a notebook
1732 1732 */
1733 1733 Notebook.prototype.fromJSON = function (data) {
1734 1734
1735 1735 var content = data.content;
1736 1736 var ncells = this.ncells();
1737 1737 var i;
1738 1738 for (i=0; i<ncells; i++) {
1739 1739 // Always delete cell 0 as they get renumbered as they are deleted.
1740 1740 this.delete_cell(0);
1741 1741 }
1742 1742 // Save the metadata and name.
1743 1743 this.metadata = content.metadata;
1744 1744 this.notebook_name = data.name;
1745 1745 this.notebook_path = data.path;
1746 1746 var trusted = true;
1747 1747
1748 1748 // Trigger an event changing the kernel spec - this will set the default
1749 1749 // codemirror mode
1750 1750 if (this.metadata.kernelspec !== undefined) {
1751 1751 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1752 1752 }
1753 1753
1754 1754 // Set the codemirror mode from language_info metadata
1755 1755 if (this.metadata.language_info !== undefined) {
1756 1756 var langinfo = this.metadata.language_info;
1757 1757 // Mode 'null' should be plain, unhighlighted text.
1758 1758 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1759 1759 this.set_codemirror_mode(cm_mode);
1760 1760 }
1761 1761
1762 1762 var new_cells = content.cells;
1763 1763 ncells = new_cells.length;
1764 1764 var cell_data = null;
1765 1765 var new_cell = null;
1766 1766 for (i=0; i<ncells; i++) {
1767 1767 cell_data = new_cells[i];
1768 1768 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1769 1769 new_cell.fromJSON(cell_data);
1770 1770 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1771 1771 trusted = false;
1772 1772 }
1773 1773 }
1774 1774 if (trusted !== this.trusted) {
1775 1775 this.trusted = trusted;
1776 1776 this.events.trigger("trust_changed.Notebook", trusted);
1777 1777 }
1778 1778 };
1779 1779
1780 1780 /**
1781 1781 * Dump this notebook into a JSON-friendly object.
1782 1782 *
1783 1783 * @return {Object} A JSON-friendly representation of this notebook.
1784 1784 */
1785 1785 Notebook.prototype.toJSON = function () {
1786 1786 // remove the conversion indicator, which only belongs in-memory
1787 1787 delete this.metadata.orig_nbformat;
1788 1788 delete this.metadata.orig_nbformat_minor;
1789 1789
1790 1790 var cells = this.get_cells();
1791 1791 var ncells = cells.length;
1792 1792 var cell_array = new Array(ncells);
1793 1793 var trusted = true;
1794 1794 for (var i=0; i<ncells; i++) {
1795 1795 var cell = cells[i];
1796 1796 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1797 1797 trusted = false;
1798 1798 }
1799 1799 cell_array[i] = cell.toJSON();
1800 1800 }
1801 1801 var data = {
1802 1802 cells: cell_array,
1803 1803 metadata: this.metadata,
1804 1804 nbformat: this.nbformat,
1805 1805 nbformat_minor: this.nbformat_minor
1806 1806 };
1807 1807 if (trusted != this.trusted) {
1808 1808 this.trusted = trusted;
1809 1809 this.events.trigger("trust_changed.Notebook", trusted);
1810 1810 }
1811 1811 return data;
1812 1812 };
1813 1813
1814 1814 /**
1815 1815 * Start an autosave timer, for periodically saving the notebook.
1816 1816 *
1817 1817 * @param {Integer} interval the autosave interval in milliseconds
1818 1818 */
1819 1819 Notebook.prototype.set_autosave_interval = function (interval) {
1820 1820 var that = this;
1821 1821 // clear previous interval, so we don't get simultaneous timers
1822 1822 if (this.autosave_timer) {
1823 1823 clearInterval(this.autosave_timer);
1824 1824 }
1825 1825 if (!this.writable) {
1826 1826 // disable autosave if not writable
1827 1827 interval = 0;
1828 1828 }
1829 1829
1830 1830 this.autosave_interval = this.minimum_autosave_interval = interval;
1831 1831 if (interval) {
1832 1832 this.autosave_timer = setInterval(function() {
1833 1833 if (that.dirty) {
1834 1834 that.save_notebook();
1835 1835 }
1836 1836 }, interval);
1837 1837 this.events.trigger("autosave_enabled.Notebook", interval);
1838 1838 } else {
1839 1839 this.autosave_timer = null;
1840 1840 this.events.trigger("autosave_disabled.Notebook");
1841 1841 }
1842 1842 };
1843 1843
1844 1844 /**
1845 1845 * Save this notebook on the server. This becomes a notebook instance's
1846 1846 * .save_notebook method *after* the entire notebook has been loaded.
1847 1847 */
1848 1848 Notebook.prototype.save_notebook = function () {
1849 1849 if (!this._fully_loaded) {
1850 1850 this.events.trigger('notebook_save_failed.Notebook',
1851 1851 new Error("Load failed, save is disabled")
1852 1852 );
1853 1853 return;
1854 1854 } else if (!this.writable) {
1855 1855 this.events.trigger('notebook_save_failed.Notebook',
1856 1856 new Error("Notebook is read-only")
1857 1857 );
1858 1858 return;
1859 1859 }
1860 1860
1861 1861 // Trigger an event before save, which allows listeners to modify
1862 1862 // the notebook as needed.
1863 1863 this.events.trigger('before_save.Notebook');
1864 1864
1865 1865 // Create a JSON model to be sent to the server.
1866 1866 var model = {
1867 1867 type : "notebook",
1868 1868 content : this.toJSON()
1869 1869 };
1870 1870 // time the ajax call for autosave tuning purposes.
1871 1871 var start = new Date().getTime();
1872 1872
1873 1873 var that = this;
1874 1874 return this.contents.save(this.notebook_path, model).then(
1875 1875 $.proxy(this.save_notebook_success, this, start),
1876 1876 function (error) {
1877 1877 that.events.trigger('notebook_save_failed.Notebook', error);
1878 1878 }
1879 1879 );
1880 1880 };
1881 1881
1882 1882 /**
1883 1883 * Success callback for saving a notebook.
1884 1884 *
1885 1885 * @param {Integer} start Time when the save request start
1886 1886 * @param {Object} data JSON representation of a notebook
1887 1887 */
1888 1888 Notebook.prototype.save_notebook_success = function (start, data) {
1889 1889 this.set_dirty(false);
1890 1890 if (data.message) {
1891 1891 // save succeeded, but validation failed.
1892 1892 var body = $("<div>");
1893 1893 var title = "Notebook validation failed";
1894 1894
1895 1895 body.append($("<p>").text(
1896 1896 "The save operation succeeded," +
1897 1897 " but the notebook does not appear to be valid." +
1898 1898 " The validation error was:"
1899 1899 )).append($("<div>").addClass("validation-error").append(
1900 1900 $("<pre>").text(data.message)
1901 1901 ));
1902 1902 dialog.modal({
1903 1903 notebook: this,
1904 1904 keyboard_manager: this.keyboard_manager,
1905 1905 title: title,
1906 1906 body: body,
1907 1907 buttons : {
1908 1908 OK : {
1909 1909 "class" : "btn-primary"
1910 1910 }
1911 1911 }
1912 1912 });
1913 1913 }
1914 1914 this.events.trigger('notebook_saved.Notebook');
1915 1915 this._update_autosave_interval(start);
1916 1916 if (this._checkpoint_after_save) {
1917 1917 this.create_checkpoint();
1918 1918 this._checkpoint_after_save = false;
1919 1919 }
1920 1920 };
1921 1921
1922 1922 /**
1923 1923 * update the autosave interval based on how long the last save took
1924 1924 *
1925 1925 * @param {Integer} timestamp when the save request started
1926 1926 * @private
1927 1927 */
1928 1928 Notebook.prototype._update_autosave_interval = function (start) {
1929 1929 var duration = (new Date().getTime() - start);
1930 1930 if (this.autosave_interval) {
1931 1931 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1932 1932 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1933 1933 // round to 10 seconds, otherwise we will be setting a new interval too often
1934 1934 interval = 10000 * Math.round(interval / 10000);
1935 1935 // set new interval, if it's changed
1936 1936 if (interval != this.autosave_interval) {
1937 1937 this.set_autosave_interval(interval);
1938 1938 }
1939 1939 }
1940 1940 };
1941 1941
1942 1942 /**
1943 1943 * Explicitly trust the output of this notebook.
1944 1944 *
1945 1945 */
1946 1946 Notebook.prototype.trust_notebook = function () {
1947 1947 var body = $("<div>").append($("<p>")
1948 1948 .text("A trusted IPython notebook may execute hidden malicious code ")
1949 1949 .append($("<strong>")
1950 1950 .append(
1951 1951 $("<em>").text("when you open it")
1952 1952 )
1953 1953 ).append(".").append(
1954 1954 " Selecting trust will immediately reload this notebook in a trusted state."
1955 1955 ).append(
1956 1956 " For more information, see the "
1957 1957 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1958 1958 .text("IPython security documentation")
1959 1959 ).append(".")
1960 1960 );
1961 1961
1962 1962 var nb = this;
1963 1963 dialog.modal({
1964 1964 notebook: this,
1965 1965 keyboard_manager: this.keyboard_manager,
1966 1966 title: "Trust this notebook?",
1967 1967 body: body,
1968 1968
1969 1969 buttons: {
1970 1970 Cancel : {},
1971 1971 Trust : {
1972 1972 class : "btn-danger",
1973 1973 click : function () {
1974 1974 var cells = nb.get_cells();
1975 1975 for (var i = 0; i < cells.length; i++) {
1976 1976 var cell = cells[i];
1977 1977 if (cell.cell_type == 'code') {
1978 1978 cell.output_area.trusted = true;
1979 1979 }
1980 1980 }
1981 1981 nb.events.on('notebook_saved.Notebook', function () {
1982 1982 window.location.reload();
1983 1983 });
1984 1984 nb.save_notebook();
1985 1985 }
1986 1986 }
1987 1987 }
1988 1988 });
1989 1989 };
1990 1990
1991 /**
1992 * Make a copy of the current notebook.
1993 * @return {null}
1994 */
1991 1995 Notebook.prototype.copy_notebook = function () {
1992 1996 var that = this;
1993 1997 var base_url = this.base_url;
1994 1998 var w = window.open();
1995 1999 var parent = utils.url_path_split(this.notebook_path)[0];
1996 2000 this.contents.copy(this.notebook_path, parent).then(
1997 2001 function (data) {
1998 2002 w.location = utils.url_join_encode(
1999 2003 base_url, 'notebooks', data.path
2000 2004 );
2001 2005 },
2002 2006 function(error) {
2003 2007 w.close();
2004 2008 that.events.trigger('notebook_copy_failed', error);
2005 2009 }
2006 2010 );
2007 2011 };
2008 2012
2013 /**
2014 * Rename the notebook
2015 * @param {string} new_name
2016 * @return {Promise} promise that resolves when the notebook is renamed.
2017 */
2009 2018 Notebook.prototype.rename = function (new_name) {
2010 2019 if (!new_name.match(/\.ipynb$/)) {
2011 2020 new_name = new_name + ".ipynb";
2012 2021 }
2013 2022
2014 2023 var that = this;
2015 2024 var parent = utils.url_path_split(this.notebook_path)[0];
2016 2025 var new_path = utils.url_path_join(parent, new_name);
2017 2026 return this.contents.rename(this.notebook_path, new_path).then(
2018 2027 function (json) {
2019 2028 that.notebook_name = json.name;
2020 2029 that.notebook_path = json.path;
2021 2030 that.session.rename_notebook(json.path);
2022 2031 that.events.trigger('notebook_renamed.Notebook', json);
2023 2032 }
2024 2033 );
2025 2034 };
2026 2035
2036 /**
2037 * Delete this notebook
2038 * @return {null}
2039 */
2027 2040 Notebook.prototype.delete = function () {
2028 2041 this.contents.delete(this.notebook_path);
2029 2042 };
2030 2043
2031 2044 /**
2032 2045 * Request a notebook's data from the server.
2033 2046 *
2034 2047 * @param {String} notebook_path A notebook to load
2035 2048 */
2036 2049 Notebook.prototype.load_notebook = function (notebook_path) {
2037 2050 this.notebook_path = notebook_path;
2038 2051 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2039 2052 this.events.trigger('notebook_loading.Notebook');
2040 2053 this.contents.get(notebook_path, {type: 'notebook'}).then(
2041 2054 $.proxy(this.load_notebook_success, this),
2042 2055 $.proxy(this.load_notebook_error, this)
2043 2056 );
2044 2057 };
2045 2058
2046 2059 /**
2047 2060 * Success callback for loading a notebook from the server.
2048 2061 *
2049 2062 * Load notebook data from the JSON response.
2050 2063 *
2051 2064 * @param {Object} data JSON representation of a notebook
2052 2065 */
2053 2066 Notebook.prototype.load_notebook_success = function (data) {
2054 2067 var failed, msg;
2055 2068 try {
2056 2069 this.fromJSON(data);
2057 2070 } catch (e) {
2058 2071 failed = e;
2059 2072 console.log("Notebook failed to load from JSON:", e);
2060 2073 }
2061 2074 if (failed || data.message) {
2062 2075 // *either* fromJSON failed or validation failed
2063 2076 var body = $("<div>");
2064 2077 var title;
2065 2078 if (failed) {
2066 2079 title = "Notebook failed to load";
2067 2080 body.append($("<p>").text(
2068 2081 "The error was: "
2069 2082 )).append($("<div>").addClass("js-error").text(
2070 2083 failed.toString()
2071 2084 )).append($("<p>").text(
2072 2085 "See the error console for details."
2073 2086 ));
2074 2087 } else {
2075 2088 title = "Notebook validation failed";
2076 2089 }
2077 2090
2078 2091 if (data.message) {
2079 2092 if (failed) {
2080 2093 msg = "The notebook also failed validation:";
2081 2094 } else {
2082 2095 msg = "An invalid notebook may not function properly." +
2083 2096 " The validation error was:";
2084 2097 }
2085 2098 body.append($("<p>").text(
2086 2099 msg
2087 2100 )).append($("<div>").addClass("validation-error").append(
2088 2101 $("<pre>").text(data.message)
2089 2102 ));
2090 2103 }
2091 2104
2092 2105 dialog.modal({
2093 2106 notebook: this,
2094 2107 keyboard_manager: this.keyboard_manager,
2095 2108 title: title,
2096 2109 body: body,
2097 2110 buttons : {
2098 2111 OK : {
2099 2112 "class" : "btn-primary"
2100 2113 }
2101 2114 }
2102 2115 });
2103 2116 }
2104 2117 if (this.ncells() === 0) {
2105 2118 this.insert_cell_below('code');
2106 2119 this.edit_mode(0);
2107 2120 } else {
2108 2121 this.select(0);
2109 2122 this.handle_command_mode(this.get_cell(0));
2110 2123 }
2111 2124 this.set_dirty(false);
2112 2125 this.scroll_to_top();
2113 2126 this.writable = data.writable || false;
2114 2127 var nbmodel = data.content;
2115 2128 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2116 2129 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2117 2130 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2118 2131 var src;
2119 2132 if (nbmodel.nbformat > orig_nbformat) {
2120 2133 src = " an older notebook format ";
2121 2134 } else {
2122 2135 src = " a newer notebook format ";
2123 2136 }
2124 2137
2125 2138 msg = "This notebook has been converted from" + src +
2126 2139 "(v"+orig_nbformat+") to the current notebook " +
2127 2140 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2128 2141 "current notebook format will be used.";
2129 2142
2130 2143 if (nbmodel.nbformat > orig_nbformat) {
2131 2144 msg += " Older versions of IPython may not be able to read the new format.";
2132 2145 } else {
2133 2146 msg += " Some features of the original notebook may not be available.";
2134 2147 }
2135 2148 msg += " To preserve the original version, close the " +
2136 2149 "notebook without saving it.";
2137 2150 dialog.modal({
2138 2151 notebook: this,
2139 2152 keyboard_manager: this.keyboard_manager,
2140 2153 title : "Notebook converted",
2141 2154 body : msg,
2142 2155 buttons : {
2143 2156 OK : {
2144 2157 class : "btn-primary"
2145 2158 }
2146 2159 }
2147 2160 });
2148 2161 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2149 2162 this.nbformat_minor = nbmodel.nbformat_minor;
2150 2163 }
2151 2164
2152 2165 // Create the session after the notebook is completely loaded to prevent
2153 2166 // code execution upon loading, which is a security risk.
2154 2167 if (this.session === null) {
2155 2168 var kernel_name;
2156 2169 if (this.metadata.kernelspec) {
2157 2170 var kernelspec = this.metadata.kernelspec || {};
2158 2171 kernel_name = kernelspec.name;
2159 2172 } else {
2160 2173 kernel_name = utils.get_url_param('kernel_name');
2161 2174 }
2162 2175 this.start_session(kernel_name);
2163 2176 }
2164 2177 // load our checkpoint list
2165 2178 this.list_checkpoints();
2166 2179
2167 2180 // load toolbar state
2168 2181 if (this.metadata.celltoolbar) {
2169 2182 celltoolbar.CellToolbar.global_show();
2170 2183 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2171 2184 } else {
2172 2185 celltoolbar.CellToolbar.global_hide();
2173 2186 }
2174 2187
2175 2188 if (!this.writable) {
2176 2189 this.set_autosave_interval(0);
2177 2190 this.events.trigger('notebook_read_only.Notebook');
2178 2191 }
2179 2192
2180 2193 // now that we're fully loaded, it is safe to restore save functionality
2181 2194 this._fully_loaded = true;
2182 2195 this.events.trigger('notebook_loaded.Notebook');
2183 2196 };
2184 2197
2185 2198 /**
2186 2199 * Failure callback for loading a notebook from the server.
2187 2200 *
2188 2201 * @param {Error} error
2189 2202 */
2190 2203 Notebook.prototype.load_notebook_error = function (error) {
2191 2204 this.events.trigger('notebook_load_failed.Notebook', error);
2192 2205 var msg;
2193 2206 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2194 2207 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2195 2208 msg = "An unknown error occurred while loading this notebook. " +
2196 2209 "This version can load notebook formats " +
2197 2210 "v" + this.nbformat + " or earlier. See the server log for details.";
2198 2211 } else {
2199 2212 msg = error.message;
2200 2213 }
2201 2214 dialog.modal({
2202 2215 notebook: this,
2203 2216 keyboard_manager: this.keyboard_manager,
2204 2217 title: "Error loading notebook",
2205 2218 body : msg,
2206 2219 buttons : {
2207 2220 "OK": {}
2208 2221 }
2209 2222 });
2210 2223 };
2211 2224
2212 2225 /********************* checkpoint-related *********************/
2213 2226
2214 2227 /**
2215 2228 * Save the notebook then immediately create a checkpoint.
2216 2229 */
2217 2230 Notebook.prototype.save_checkpoint = function () {
2218 2231 this._checkpoint_after_save = true;
2219 2232 this.save_notebook();
2220 2233 };
2221 2234
2222 2235 /**
2223 2236 * Add a checkpoint for this notebook.
2224 2237 * for use as a callback from checkpoint creation.
2225 2238 */
2226 2239 Notebook.prototype.add_checkpoint = function (checkpoint) {
2227 2240 var found = false;
2228 2241 for (var i = 0; i < this.checkpoints.length; i++) {
2229 2242 var existing = this.checkpoints[i];
2230 2243 if (existing.id == checkpoint.id) {
2231 2244 found = true;
2232 2245 this.checkpoints[i] = checkpoint;
2233 2246 break;
2234 2247 }
2235 2248 }
2236 2249 if (!found) {
2237 2250 this.checkpoints.push(checkpoint);
2238 2251 }
2239 2252 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2240 2253 };
2241 2254
2242 2255 /**
2243 2256 * List checkpoints for this notebook.
2244 2257 */
2245 2258 Notebook.prototype.list_checkpoints = function () {
2246 2259 var that = this;
2247 2260 this.contents.list_checkpoints(this.notebook_path).then(
2248 2261 $.proxy(this.list_checkpoints_success, this),
2249 2262 function(error) {
2250 2263 that.events.trigger('list_checkpoints_failed.Notebook', error);
2251 2264 }
2252 2265 );
2253 2266 };
2254 2267
2255 2268 /**
2256 2269 * Success callback for listing checkpoints.
2257 2270 *
2258 2271 * @param {Object} data JSON representation of a checkpoint
2259 2272 */
2260 2273 Notebook.prototype.list_checkpoints_success = function (data) {
2261 2274 this.checkpoints = data;
2262 2275 if (data.length) {
2263 2276 this.last_checkpoint = data[data.length - 1];
2264 2277 } else {
2265 2278 this.last_checkpoint = null;
2266 2279 }
2267 2280 this.events.trigger('checkpoints_listed.Notebook', [data]);
2268 2281 };
2269 2282
2270 2283 /**
2271 2284 * Create a checkpoint of this notebook on the server from the most recent save.
2272 2285 */
2273 2286 Notebook.prototype.create_checkpoint = function () {
2274 2287 var that = this;
2275 2288 this.contents.create_checkpoint(this.notebook_path).then(
2276 2289 $.proxy(this.create_checkpoint_success, this),
2277 2290 function (error) {
2278 2291 that.events.trigger('checkpoint_failed.Notebook', error);
2279 2292 }
2280 2293 );
2281 2294 };
2282 2295
2283 2296 /**
2284 2297 * Success callback for creating a checkpoint.
2285 2298 *
2286 2299 * @param {Object} data JSON representation of a checkpoint
2287 2300 */
2288 2301 Notebook.prototype.create_checkpoint_success = function (data) {
2289 2302 this.add_checkpoint(data);
2290 2303 this.events.trigger('checkpoint_created.Notebook', data);
2291 2304 };
2292 2305
2293 2306 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2294 2307 var that = this;
2295 2308 checkpoint = checkpoint || this.last_checkpoint;
2296 2309 if ( ! checkpoint ) {
2297 2310 console.log("restore dialog, but no checkpoint to restore to!");
2298 2311 return;
2299 2312 }
2300 2313 var body = $('<div/>').append(
2301 2314 $('<p/>').addClass("p-space").text(
2302 2315 "Are you sure you want to revert the notebook to " +
2303 2316 "the latest checkpoint?"
2304 2317 ).append(
2305 2318 $("<strong/>").text(
2306 2319 " This cannot be undone."
2307 2320 )
2308 2321 )
2309 2322 ).append(
2310 2323 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2311 2324 ).append(
2312 2325 $('<p/>').addClass("p-space").text(
2313 2326 Date(checkpoint.last_modified)
2314 2327 ).css("text-align", "center")
2315 2328 );
2316 2329
2317 2330 dialog.modal({
2318 2331 notebook: this,
2319 2332 keyboard_manager: this.keyboard_manager,
2320 2333 title : "Revert notebook to checkpoint",
2321 2334 body : body,
2322 2335 buttons : {
2323 2336 Revert : {
2324 2337 class : "btn-danger",
2325 2338 click : function () {
2326 2339 that.restore_checkpoint(checkpoint.id);
2327 2340 }
2328 2341 },
2329 2342 Cancel : {}
2330 2343 }
2331 2344 });
2332 2345 };
2333 2346
2334 2347 /**
2335 2348 * Restore the notebook to a checkpoint state.
2336 2349 *
2337 2350 * @param {String} checkpoint ID
2338 2351 */
2339 2352 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2340 2353 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2341 2354 var that = this;
2342 2355 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2343 2356 $.proxy(this.restore_checkpoint_success, this),
2344 2357 function (error) {
2345 2358 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2346 2359 }
2347 2360 );
2348 2361 };
2349 2362
2350 2363 /**
2351 2364 * Success callback for restoring a notebook to a checkpoint.
2352 2365 */
2353 2366 Notebook.prototype.restore_checkpoint_success = function () {
2354 2367 this.events.trigger('checkpoint_restored.Notebook');
2355 2368 this.load_notebook(this.notebook_path);
2356 2369 };
2357 2370
2358 2371 /**
2359 2372 * Delete a notebook checkpoint.
2360 2373 *
2361 2374 * @param {String} checkpoint ID
2362 2375 */
2363 2376 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2364 2377 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2365 2378 var that = this;
2366 2379 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2367 2380 $.proxy(this.delete_checkpoint_success, this),
2368 2381 function (error) {
2369 2382 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2370 2383 }
2371 2384 );
2372 2385 };
2373 2386
2374 2387 /**
2375 2388 * Success callback for deleting a notebook checkpoint
2376 2389 */
2377 2390 Notebook.prototype.delete_checkpoint_success = function () {
2378 2391 this.events.trigger('checkpoint_deleted.Notebook');
2379 2392 this.load_notebook(this.notebook_path);
2380 2393 };
2381 2394
2382 2395
2383 2396 // For backwards compatability.
2384 2397 IPython.Notebook = Notebook;
2385 2398
2386 2399 return {'Notebook': Notebook};
2387 2400 });
General Comments 0
You need to be logged in to leave comments. Login now