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