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