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