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