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