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