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