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