##// END OF EJS Templates
Merge pull request #7207 from takluyver/rm-nb-user-config...
Matthias Bussonnier -
r19551:a1acff00 merge
parent child Browse files
Show More
@@ -1,603 +1,629 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3 /**
4 4 *
5 5 *
6 6 * @module codecell
7 7 * @namespace codecell
8 8 * @class CodeCell
9 9 */
10 10
11 11
12 12 define([
13 13 'base/js/namespace',
14 14 'jquery',
15 15 'base/js/utils',
16 16 'base/js/keyboard',
17 'services/config',
17 18 'notebook/js/cell',
18 19 'notebook/js/outputarea',
19 20 'notebook/js/completer',
20 21 'notebook/js/celltoolbar',
21 22 'codemirror/lib/codemirror',
22 23 'codemirror/mode/python/python',
23 24 'notebook/js/codemirror-ipython'
24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
25 ], function(IPython,
26 $,
27 utils,
28 keyboard,
29 configmod,
30 cell,
31 outputarea,
32 completer,
33 celltoolbar,
34 CodeMirror,
35 cmpython,
36 cmip
37 ) {
25 38 "use strict";
26 39
27 40 var Cell = cell.Cell;
28 41
29 42 /* local util for codemirror */
30 43 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
31 44
32 45 /**
33 46 *
34 47 * function to delete until previous non blanking space character
35 48 * or first multiple of 4 tabstop.
36 49 * @private
37 50 */
38 51 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
39 52 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
40 53 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
41 54 var cur = cm.getCursor(), line = cm.getLine(cur.line);
42 55 var tabsize = cm.getOption('tabSize');
43 56 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
44 57 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
45 58 var select = cm.getRange(from,cur);
46 59 if( select.match(/^\ +$/) !== null){
47 60 cm.replaceRange("",from,cur);
48 61 } else {
49 62 cm.deleteH(-1,"char");
50 63 }
51 64 };
52 65
53 66 var keycodes = keyboard.keycodes;
54 67
55 68 var CodeCell = function (kernel, options) {
56 69 /**
57 70 * Constructor
58 71 *
59 72 * A Cell conceived to write code.
60 73 *
61 74 * Parameters:
62 75 * kernel: Kernel instance
63 76 * The kernel doesn't have to be set at creation time, in that case
64 77 * it will be null and set_kernel has to be called later.
65 78 * options: dictionary
66 79 * Dictionary of keyword arguments.
67 80 * events: $(Events) instance
68 81 * config: dictionary
69 82 * keyboard_manager: KeyboardManager instance
70 83 * notebook: Notebook instance
71 84 * tooltip: Tooltip instance
72 85 */
73 86 this.kernel = kernel || null;
74 87 this.notebook = options.notebook;
75 88 this.collapsed = false;
76 89 this.events = options.events;
77 90 this.tooltip = options.tooltip;
78 91 this.config = options.config;
92 this.class_config = new configmod.ConfigWithDefaults(this.config,
93 CodeCell.config_defaults, 'CodeCell');
79 94
80 95 // create all attributed in constructor function
81 96 // even if null for V8 VM optimisation
82 97 this.input_prompt_number = null;
83 98 this.celltoolbar = null;
84 99 this.output_area = null;
85 100 // Keep a stack of the 'active' output areas (where active means the
86 101 // output area that recieves output). When a user activates an output
87 102 // area, it gets pushed to the stack. Then, when the output area is
88 103 // deactivated, it's popped from the stack. When the stack is empty,
89 104 // the cell's output area is used.
90 105 this.active_output_areas = [];
91 106 var that = this;
92 107 Object.defineProperty(this, 'active_output_area', {
93 108 get: function() {
94 109 if (that.active_output_areas && that.active_output_areas.length > 0) {
95 110 return that.active_output_areas[that.active_output_areas.length-1];
96 111 } else {
97 112 return that.output_area;
98 113 }
99 114 },
100 115 });
101 116
102 117 this.last_msg_id = null;
103 118 this.completer = null;
104 119 this.widget_views = [];
105 120
106 var config = utils.mergeopt(CodeCell, this.config);
107 121 Cell.apply(this,[{
108 config: config,
122 config: $.extend({}, CodeCell.options_default),
109 123 keyboard_manager: options.keyboard_manager,
110 124 events: this.events}]);
111 125
112 126 // Attributes we want to override in this subclass.
113 127 this.cell_type = "code";
114 128 this.element.focusout(
115 129 function() { that.auto_highlight(); }
116 130 );
117 131 };
118 132
119 133 CodeCell.options_default = {
120 134 cm_config : {
121 135 extraKeys: {
122 136 "Tab" : "indentMore",
123 137 "Shift-Tab" : "indentLess",
124 138 "Backspace" : "delSpaceToPrevTabStop",
125 139 "Cmd-/" : "toggleComment",
126 140 "Ctrl-/" : "toggleComment"
127 141 },
128 142 mode: 'ipython',
129 143 theme: 'ipython',
130 144 matchBrackets: true
131 145 }
132 146 };
133 147
148 CodeCell.config_defaults = {
149 cell_magic_highlight : {
150 'magic_javascript' :{'reg':[/^%%javascript/]},
151 'magic_perl' :{'reg':[/^%%perl/]},
152 'magic_ruby' :{'reg':[/^%%ruby/]},
153 'magic_python' :{'reg':[/^%%python3?/]},
154 'magic_shell' :{'reg':[/^%%bash/]},
155 'magic_r' :{'reg':[/^%%R/]},
156 'magic_text/x-cython' :{'reg':[/^%%cython/]},
157 },
158 };
159
134 160 CodeCell.msg_cells = {};
135 161
136 162 CodeCell.prototype = Object.create(Cell.prototype);
137 163
138 164 /**
139 165 * @method push_output_area
140 166 */
141 167 CodeCell.prototype.push_output_area = function (output_area) {
142 168 this.active_output_areas.push(output_area);
143 169 };
144 170
145 171 /**
146 172 * @method pop_output_area
147 173 */
148 174 CodeCell.prototype.pop_output_area = function (output_area) {
149 175 var index = this.active_output_areas.lastIndexOf(output_area);
150 176 if (index > -1) {
151 177 this.active_output_areas.splice(index, 1);
152 178 }
153 179 };
154 180
155 181 /**
156 182 * @method auto_highlight
157 183 */
158 184 CodeCell.prototype.auto_highlight = function () {
159 this._auto_highlight(this.config.cell_magic_highlight);
185 this._auto_highlight(this.class_config.get_sync('cell_magic_highlight'));
160 186 };
161 187
162 188 /** @method create_element */
163 189 CodeCell.prototype.create_element = function () {
164 190 Cell.prototype.create_element.apply(this, arguments);
165 191
166 192 var cell = $('<div></div>').addClass('cell code_cell');
167 193 cell.attr('tabindex','2');
168 194
169 195 var input = $('<div></div>').addClass('input');
170 196 var prompt = $('<div/>').addClass('prompt input_prompt');
171 197 var inner_cell = $('<div/>').addClass('inner_cell');
172 198 this.celltoolbar = new celltoolbar.CellToolbar({
173 199 cell: this,
174 200 notebook: this.notebook});
175 201 inner_cell.append(this.celltoolbar.element);
176 202 var input_area = $('<div/>').addClass('input_area');
177 203 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
178 204 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
179 205 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
180 206 inner_cell.append(input_area);
181 207 input.append(prompt).append(inner_cell);
182 208
183 209 var widget_area = $('<div/>')
184 210 .addClass('widget-area')
185 211 .hide();
186 212 this.widget_area = widget_area;
187 213 var widget_prompt = $('<div/>')
188 214 .addClass('prompt')
189 215 .appendTo(widget_area);
190 216 var widget_subarea = $('<div/>')
191 217 .addClass('widget-subarea')
192 218 .appendTo(widget_area);
193 219 this.widget_subarea = widget_subarea;
194 220 var that = this;
195 221 var widget_clear_buton = $('<button />')
196 222 .addClass('close')
197 223 .html('&times;')
198 224 .click(function() {
199 225 widget_area.slideUp('', function(){
200 226 for (var i = 0; i < that.widget_views.length; i++) {
201 227 that.widget_views[i].remove();
202 228 }
203 229 that.widget_views = [];
204 230 widget_subarea.html('');
205 231 });
206 232 })
207 233 .appendTo(widget_prompt);
208 234
209 235 var output = $('<div></div>');
210 236 cell.append(input).append(widget_area).append(output);
211 237 this.element = cell;
212 238 this.output_area = new outputarea.OutputArea({
213 239 selector: output,
214 240 prompt_area: true,
215 241 events: this.events,
216 242 keyboard_manager: this.keyboard_manager});
217 243 this.completer = new completer.Completer(this, this.events);
218 244 };
219 245
220 246 /**
221 247 * Display a widget view in the cell.
222 248 */
223 249 CodeCell.prototype.display_widget_view = function(view_promise) {
224 250
225 251 // Display a dummy element
226 252 var dummy = $('<div/>');
227 253 this.widget_subarea.append(dummy);
228 254
229 255 // Display the view.
230 256 var that = this;
231 257 return view_promise.then(function(view) {
232 258 that.widget_area.show();
233 259 dummy.replaceWith(view.$el);
234 260 that.widget_views.push(view);
235 261 return view;
236 262 });
237 263 };
238 264
239 265 /** @method bind_events */
240 266 CodeCell.prototype.bind_events = function () {
241 267 Cell.prototype.bind_events.apply(this);
242 268 var that = this;
243 269
244 270 this.element.focusout(
245 271 function() { that.auto_highlight(); }
246 272 );
247 273 };
248 274
249 275
250 276 /**
251 277 * This method gets called in CodeMirror's onKeyDown/onKeyPress
252 278 * handlers and is used to provide custom key handling. Its return
253 279 * value is used to determine if CodeMirror should ignore the event:
254 280 * true = ignore, false = don't ignore.
255 281 * @method handle_codemirror_keyevent
256 282 */
257 283
258 284 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
259 285
260 286 var that = this;
261 287 // whatever key is pressed, first, cancel the tooltip request before
262 288 // they are sent, and remove tooltip if any, except for tab again
263 289 var tooltip_closed = null;
264 290 if (event.type === 'keydown' && event.which != keycodes.tab ) {
265 291 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
266 292 }
267 293
268 294 var cur = editor.getCursor();
269 295 if (event.keyCode === keycodes.enter){
270 296 this.auto_highlight();
271 297 }
272 298
273 299 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
274 300 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
275 301 // browser and keyboard layout !
276 302 // Pressing '(' , request tooltip, don't forget to reappend it
277 303 // The second argument says to hide the tooltip if the docstring
278 304 // is actually empty
279 305 this.tooltip.pending(that, true);
280 306 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
281 307 // If tooltip is active, cancel it. The call to
282 308 // remove_and_cancel_tooltip above doesn't pass, force=true.
283 309 // Because of this it won't actually close the tooltip
284 310 // if it is in sticky mode. Thus, we have to check again if it is open
285 311 // and close it with force=true.
286 312 if (!this.tooltip._hidden) {
287 313 this.tooltip.remove_and_cancel_tooltip(true);
288 314 }
289 315 // If we closed the tooltip, don't let CM or the global handlers
290 316 // handle this event.
291 317 event.codemirrorIgnore = true;
292 318 event.preventDefault();
293 319 return true;
294 320 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
295 321 if (editor.somethingSelected() || editor.getSelections().length !== 1){
296 322 var anchor = editor.getCursor("anchor");
297 323 var head = editor.getCursor("head");
298 324 if( anchor.line != head.line){
299 325 return false;
300 326 }
301 327 }
302 328 this.tooltip.request(that);
303 329 event.codemirrorIgnore = true;
304 330 event.preventDefault();
305 331 return true;
306 332 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
307 333 // Tab completion.
308 334 this.tooltip.remove_and_cancel_tooltip();
309 335
310 336 // completion does not work on multicursor, it might be possible though in some cases
311 337 if (editor.somethingSelected() || editor.getSelections().length > 1) {
312 338 return false;
313 339 }
314 340 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
315 341 if (pre_cursor.trim() === "") {
316 342 // Don't autocomplete if the part of the line before the cursor
317 343 // is empty. In this case, let CodeMirror handle indentation.
318 344 return false;
319 345 } else {
320 346 event.codemirrorIgnore = true;
321 347 event.preventDefault();
322 348 this.completer.startCompletion();
323 349 return true;
324 350 }
325 351 }
326 352
327 353 // keyboard event wasn't one of those unique to code cells, let's see
328 354 // if it's one of the generic ones (i.e. check edit mode shortcuts)
329 355 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
330 356 };
331 357
332 358 // Kernel related calls.
333 359
334 360 CodeCell.prototype.set_kernel = function (kernel) {
335 361 this.kernel = kernel;
336 362 };
337 363
338 364 /**
339 365 * Execute current code cell to the kernel
340 366 * @method execute
341 367 */
342 368 CodeCell.prototype.execute = function () {
343 369 if (!this.kernel || !this.kernel.is_connected()) {
344 370 console.log("Can't execute, kernel is not connected.");
345 371 return;
346 372 }
347 373
348 374 this.active_output_area.clear_output();
349 375
350 376 // Clear widget area
351 377 for (var i = 0; i < this.widget_views.length; i++) {
352 378 this.widget_views[i].remove();
353 379 }
354 380 this.widget_views = [];
355 381 this.widget_subarea.html('');
356 382 this.widget_subarea.height('');
357 383 this.widget_area.height('');
358 384 this.widget_area.hide();
359 385
360 386 this.set_input_prompt('*');
361 387 this.element.addClass("running");
362 388 if (this.last_msg_id) {
363 389 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
364 390 }
365 391 var callbacks = this.get_callbacks();
366 392
367 393 var old_msg_id = this.last_msg_id;
368 394 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
369 395 if (old_msg_id) {
370 396 delete CodeCell.msg_cells[old_msg_id];
371 397 }
372 398 CodeCell.msg_cells[this.last_msg_id] = this;
373 399 this.render();
374 400 this.events.trigger('execute.CodeCell', {cell: this});
375 401 };
376 402
377 403 /**
378 404 * Construct the default callbacks for
379 405 * @method get_callbacks
380 406 */
381 407 CodeCell.prototype.get_callbacks = function () {
382 408 var that = this;
383 409 return {
384 410 shell : {
385 411 reply : $.proxy(this._handle_execute_reply, this),
386 412 payload : {
387 413 set_next_input : $.proxy(this._handle_set_next_input, this),
388 414 page : $.proxy(this._open_with_pager, this)
389 415 }
390 416 },
391 417 iopub : {
392 418 output : function() {
393 419 that.active_output_area.handle_output.apply(that.active_output_area, arguments);
394 420 },
395 421 clear_output : function() {
396 422 that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments);
397 423 },
398 424 },
399 425 input : $.proxy(this._handle_input_request, this)
400 426 };
401 427 };
402 428
403 429 CodeCell.prototype._open_with_pager = function (payload) {
404 430 this.events.trigger('open_with_text.Pager', payload);
405 431 };
406 432
407 433 /**
408 434 * @method _handle_execute_reply
409 435 * @private
410 436 */
411 437 CodeCell.prototype._handle_execute_reply = function (msg) {
412 438 this.set_input_prompt(msg.content.execution_count);
413 439 this.element.removeClass("running");
414 440 this.events.trigger('set_dirty.Notebook', {value: true});
415 441 };
416 442
417 443 /**
418 444 * @method _handle_set_next_input
419 445 * @private
420 446 */
421 447 CodeCell.prototype._handle_set_next_input = function (payload) {
422 448 var data = {'cell': this, 'text': payload.text, replace: payload.replace};
423 449 this.events.trigger('set_next_input.Notebook', data);
424 450 };
425 451
426 452 /**
427 453 * @method _handle_input_request
428 454 * @private
429 455 */
430 456 CodeCell.prototype._handle_input_request = function (msg) {
431 457 this.active_output_area.append_raw_input(msg);
432 458 };
433 459
434 460
435 461 // Basic cell manipulation.
436 462
437 463 CodeCell.prototype.select = function () {
438 464 var cont = Cell.prototype.select.apply(this);
439 465 if (cont) {
440 466 this.code_mirror.refresh();
441 467 this.auto_highlight();
442 468 }
443 469 return cont;
444 470 };
445 471
446 472 CodeCell.prototype.render = function () {
447 473 var cont = Cell.prototype.render.apply(this);
448 474 // Always execute, even if we are already in the rendered state
449 475 return cont;
450 476 };
451 477
452 478 CodeCell.prototype.select_all = function () {
453 479 var start = {line: 0, ch: 0};
454 480 var nlines = this.code_mirror.lineCount();
455 481 var last_line = this.code_mirror.getLine(nlines-1);
456 482 var end = {line: nlines-1, ch: last_line.length};
457 483 this.code_mirror.setSelection(start, end);
458 484 };
459 485
460 486
461 487 CodeCell.prototype.collapse_output = function () {
462 488 this.output_area.collapse();
463 489 };
464 490
465 491
466 492 CodeCell.prototype.expand_output = function () {
467 493 this.output_area.expand();
468 494 this.output_area.unscroll_area();
469 495 };
470 496
471 497 CodeCell.prototype.scroll_output = function () {
472 498 this.output_area.expand();
473 499 this.output_area.scroll_if_long();
474 500 };
475 501
476 502 CodeCell.prototype.toggle_output = function () {
477 503 this.output_area.toggle_output();
478 504 };
479 505
480 506 CodeCell.prototype.toggle_output_scroll = function () {
481 507 this.output_area.toggle_scroll();
482 508 };
483 509
484 510
485 511 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
486 512 var ns;
487 513 if (prompt_value === undefined || prompt_value === null) {
488 514 ns = "&nbsp;";
489 515 } else {
490 516 ns = encodeURIComponent(prompt_value);
491 517 }
492 518 return 'In&nbsp;[' + ns + ']:';
493 519 };
494 520
495 521 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
496 522 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
497 523 for(var i=1; i < lines_number; i++) {
498 524 html.push(['...:']);
499 525 }
500 526 return html.join('<br/>');
501 527 };
502 528
503 529 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
504 530
505 531
506 532 CodeCell.prototype.set_input_prompt = function (number) {
507 533 var nline = 1;
508 534 if (this.code_mirror !== undefined) {
509 535 nline = this.code_mirror.lineCount();
510 536 }
511 537 this.input_prompt_number = number;
512 538 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
513 539 // This HTML call is okay because the user contents are escaped.
514 540 this.element.find('div.input_prompt').html(prompt_html);
515 541 };
516 542
517 543
518 544 CodeCell.prototype.clear_input = function () {
519 545 this.code_mirror.setValue('');
520 546 };
521 547
522 548
523 549 CodeCell.prototype.get_text = function () {
524 550 return this.code_mirror.getValue();
525 551 };
526 552
527 553
528 554 CodeCell.prototype.set_text = function (code) {
529 555 return this.code_mirror.setValue(code);
530 556 };
531 557
532 558
533 559 CodeCell.prototype.clear_output = function (wait) {
534 560 this.active_output_area.clear_output(wait);
535 561 this.set_input_prompt();
536 562 };
537 563
538 564
539 565 // JSON serialization
540 566
541 567 CodeCell.prototype.fromJSON = function (data) {
542 568 Cell.prototype.fromJSON.apply(this, arguments);
543 569 if (data.cell_type === 'code') {
544 570 if (data.source !== undefined) {
545 571 this.set_text(data.source);
546 572 // make this value the starting point, so that we can only undo
547 573 // to this state, instead of a blank cell
548 574 this.code_mirror.clearHistory();
549 575 this.auto_highlight();
550 576 }
551 577 this.set_input_prompt(data.execution_count);
552 578 this.output_area.trusted = data.metadata.trusted || false;
553 579 this.output_area.fromJSON(data.outputs);
554 580 if (data.metadata.collapsed !== undefined) {
555 581 if (data.metadata.collapsed) {
556 582 this.collapse_output();
557 583 } else {
558 584 this.expand_output();
559 585 }
560 586 }
561 587 }
562 588 };
563 589
564 590
565 591 CodeCell.prototype.toJSON = function () {
566 592 var data = Cell.prototype.toJSON.apply(this);
567 593 data.source = this.get_text();
568 594 // is finite protect against undefined and '*' value
569 595 if (isFinite(this.input_prompt_number)) {
570 596 data.execution_count = this.input_prompt_number;
571 597 } else {
572 598 data.execution_count = null;
573 599 }
574 600 var outputs = this.output_area.toJSON();
575 601 data.outputs = outputs;
576 602 data.metadata.trusted = this.output_area.trusted;
577 603 data.metadata.collapsed = this.output_area.collapsed;
578 604 return data;
579 605 };
580 606
581 607 /**
582 608 * handle cell level logic when a cell is unselected
583 609 * @method unselect
584 610 * @return is the action being taken
585 611 */
586 612 CodeCell.prototype.unselect = function () {
587 613 var cont = Cell.prototype.unselect.apply(this);
588 614 if (cont) {
589 615 // When a code cell is usnelected, make sure that the corresponding
590 616 // tooltip and completer to that cell is closed.
591 617 this.tooltip.remove_and_cancel_tooltip(true);
592 618 if (this.completer !== null) {
593 619 this.completer.close();
594 620 }
595 621 }
596 622 return cont;
597 623 };
598 624
599 625 // Backwards compatability.
600 626 IPython.CodeCell = CodeCell;
601 627
602 628 return {'CodeCell': CodeCell};
603 629 });
@@ -1,166 +1,162 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 require([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'notebook/js/notebook',
8 8 'contents',
9 9 'services/config',
10 10 'base/js/utils',
11 11 'base/js/page',
12 12 'base/js/events',
13 13 'auth/js/loginwidget',
14 14 'notebook/js/maintoolbar',
15 15 'notebook/js/pager',
16 16 'notebook/js/quickhelp',
17 17 'notebook/js/menubar',
18 18 'notebook/js/notificationarea',
19 19 'notebook/js/savewidget',
20 20 'notebook/js/actions',
21 21 'notebook/js/keyboardmanager',
22 'notebook/js/config',
23 22 'notebook/js/kernelselector',
24 23 'codemirror/lib/codemirror',
25 24 'notebook/js/about',
26 25 // only loaded, not used, please keep sure this is loaded last
27 26 'custom/custom'
28 27 ], function(
29 28 IPython,
30 29 $,
31 30 notebook,
32 31 contents,
33 32 configmod,
34 33 utils,
35 34 page,
36 35 events,
37 36 loginwidget,
38 37 maintoolbar,
39 38 pager,
40 39 quickhelp,
41 40 menubar,
42 41 notificationarea,
43 42 savewidget,
44 43 actions,
45 44 keyboardmanager,
46 config,
47 45 kernelselector,
48 46 CodeMirror,
49 47 about,
50 48 // please keep sure that even if not used, this is loaded last
51 49 custom
52 50 ) {
53 51 "use strict";
54 52
55 53 // compat with old IPython, remove for IPython > 3.0
56 54 window.CodeMirror = CodeMirror;
57 55
58 56 var common_options = {
59 57 ws_url : utils.get_body_data("wsUrl"),
60 58 base_url : utils.get_body_data("baseUrl"),
61 59 notebook_path : utils.get_body_data("notebookPath"),
62 60 notebook_name : utils.get_body_data('notebookName')
63 61 };
64 62
65 var user_config = $.extend({}, config.default_config);
66 63 var page = new page.Page();
67 64 var pager = new pager.Pager('div#pager', {
68 65 events: events});
69 66 var acts = new actions.init();
70 67 var keyboard_manager = new keyboardmanager.KeyboardManager({
71 68 pager: pager,
72 69 events: events,
73 70 actions: acts });
74 71 var save_widget = new savewidget.SaveWidget('span#save_widget', {
75 72 events: events,
76 73 keyboard_manager: keyboard_manager});
77 74 var contents = new contents.Contents($.extend({
78 75 events: events},
79 76 common_options));
80 77 var config_section = new configmod.ConfigSection('notebook', common_options);
81 78 config_section.load();
82 79 var notebook = new notebook.Notebook('div#notebook', $.extend({
83 80 events: events,
84 81 keyboard_manager: keyboard_manager,
85 82 save_widget: save_widget,
86 83 contents: contents,
87 config: user_config},
84 config: config_section},
88 85 common_options));
89 86 var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
90 87 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
91 88 notebook: notebook,
92 89 events: events,
93 90 actions: acts});
94 91 var quick_help = new quickhelp.QuickHelp({
95 92 keyboard_manager: keyboard_manager,
96 93 events: events,
97 94 notebook: notebook});
98 95 keyboard_manager.set_notebook(notebook);
99 96 keyboard_manager.set_quickhelp(quick_help);
100 97 var menubar = new menubar.MenuBar('#menubar', $.extend({
101 98 notebook: notebook,
102 99 contents: contents,
103 100 events: events,
104 101 save_widget: save_widget,
105 102 quick_help: quick_help},
106 103 common_options));
107 104 var notification_area = new notificationarea.NotebookNotificationArea(
108 105 '#notification_area', {
109 106 events: events,
110 107 save_widget: save_widget,
111 108 notebook: notebook,
112 109 keyboard_manager: keyboard_manager});
113 110 notification_area.init_notification_widgets();
114 111 var kernel_selector = new kernelselector.KernelSelector(
115 112 '#kernel_selector_widget', notebook);
116 113
117 114 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
118 115 '<span id="test2" style="font-weight: bold;">x</span>'+
119 116 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
120 117 var nh = $('#test1').innerHeight();
121 118 var bh = $('#test2').innerHeight();
122 119 var ih = $('#test3').innerHeight();
123 120 if(nh != bh || nh != ih) {
124 121 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
125 122 }
126 123 $('#fonttest').remove();
127 124
128 125 page.show();
129 126
130 127 var first_load = function () {
131 128 var hash = document.location.hash;
132 129 if (hash) {
133 130 document.location.hash = '';
134 131 document.location.hash = hash;
135 132 }
136 133 notebook.set_autosave_interval(notebook.minimum_autosave_interval);
137 134 // only do this once
138 135 events.off('notebook_loaded.Notebook', first_load);
139 136 };
140 137 events.on('notebook_loaded.Notebook', first_load);
141 138
142 139 IPython.page = page;
143 140 IPython.notebook = notebook;
144 141 IPython.contents = contents;
145 142 IPython.pager = pager;
146 143 IPython.quick_help = quick_help;
147 144 IPython.login_widget = login_widget;
148 145 IPython.menubar = menubar;
149 146 IPython.toolbar = toolbar;
150 147 IPython.notification_area = notification_area;
151 148 IPython.keyboard_manager = keyboard_manager;
152 149 IPython.save_widget = save_widget;
153 IPython.config = user_config;
154 150 IPython.tooltip = notebook.tooltip;
155 151
156 152 events.trigger('app_initialized.NotebookApp');
157 153 config_section.loaded.then(function() {
158 154 if (config_section.data.load_extensions) {
159 155 var nbextension_paths = Object.getOwnPropertyNames(
160 156 config_section.data.load_extensions);
161 157 IPython.load_extensions.apply(this, nbextension_paths);
162 158 }
163 159 });
164 160 notebook.load_notebook(common_options.notebook_path);
165 161
166 162 });
@@ -1,2395 +1,2396 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 /**
5 5 * @module notebook
6 6 */
7 7 define([
8 8 'base/js/namespace',
9 9 'jquery',
10 10 'base/js/utils',
11 11 'base/js/dialog',
12 12 'notebook/js/cell',
13 13 'notebook/js/textcell',
14 14 'notebook/js/codecell',
15 'services/config',
15 16 'services/sessions/session',
16 17 'notebook/js/celltoolbar',
17 18 'components/marked/lib/marked',
18 19 'codemirror/lib/codemirror',
19 20 'codemirror/addon/runmode/runmode',
20 21 'notebook/js/mathjaxutils',
21 22 'base/js/keyboard',
22 23 'notebook/js/tooltip',
23 24 'notebook/js/celltoolbarpresets/default',
24 25 'notebook/js/celltoolbarpresets/rawcell',
25 26 'notebook/js/celltoolbarpresets/slideshow',
26 27 'notebook/js/scrollmanager'
27 28 ], function (
28 29 IPython,
29 30 $,
30 31 utils,
31 32 dialog,
32 33 cellmod,
33 34 textcell,
34 35 codecell,
36 configmod,
35 37 session,
36 38 celltoolbar,
37 39 marked,
38 40 CodeMirror,
39 41 runMode,
40 42 mathjaxutils,
41 43 keyboard,
42 44 tooltip,
43 45 default_celltoolbar,
44 46 rawcell_celltoolbar,
45 47 slideshow_celltoolbar,
46 48 scrollmanager
47 49 ) {
48 50 "use strict";
49 51
50 52 /**
51 53 * Contains and manages cells.
52 54 *
53 55 * @class Notebook
54 56 * @param {string} selector
55 57 * @param {object} options - Dictionary of keyword arguments.
56 58 * @param {jQuery} options.events - selector of Events
57 59 * @param {KeyboardManager} options.keyboard_manager
58 60 * @param {Contents} options.contents
59 61 * @param {SaveWidget} options.save_widget
60 62 * @param {object} options.config
61 63 * @param {string} options.base_url
62 64 * @param {string} options.notebook_path
63 65 * @param {string} options.notebook_name
64 66 */
65 67 var Notebook = function (selector, options) {
66 this.config = utils.mergeopt(Notebook, options.config);
68 this.config = options.config;
69 this.class_config = new configmod.ConfigWithDefaults(this.config,
70 Notebook.options_default, 'Notebook');
67 71 this.base_url = options.base_url;
68 72 this.notebook_path = options.notebook_path;
69 73 this.notebook_name = options.notebook_name;
70 74 this.events = options.events;
71 75 this.keyboard_manager = options.keyboard_manager;
72 76 this.contents = options.contents;
73 77 this.save_widget = options.save_widget;
74 78 this.tooltip = new tooltip.Tooltip(this.events);
75 79 this.ws_url = options.ws_url;
76 80 this._session_starting = false;
77 this.default_cell_type = this.config.default_cell_type || 'code';
78 81
79 82 // Create default scroll manager.
80 83 this.scroll_manager = new scrollmanager.ScrollManager(this);
81 84
82 85 // TODO: This code smells (and the other `= this` line a couple lines down)
83 86 // We need a better way to deal with circular instance references.
84 87 this.keyboard_manager.notebook = this;
85 88 this.save_widget.notebook = this;
86 89
87 90 mathjaxutils.init();
88 91
89 92 if (marked) {
90 93 marked.setOptions({
91 94 gfm : true,
92 95 tables: true,
93 96 // FIXME: probably want central config for CodeMirror theme when we have js config
94 97 langPrefix: "cm-s-ipython language-",
95 98 highlight: function(code, lang, callback) {
96 99 if (!lang) {
97 100 // no language, no highlight
98 101 if (callback) {
99 102 callback(null, code);
100 103 return;
101 104 } else {
102 105 return code;
103 106 }
104 107 }
105 108 utils.requireCodeMirrorMode(lang, function (spec) {
106 109 var el = document.createElement("div");
107 110 var mode = CodeMirror.getMode({}, spec);
108 111 if (!mode) {
109 112 console.log("No CodeMirror mode: " + lang);
110 113 callback(null, code);
111 114 return;
112 115 }
113 116 try {
114 117 CodeMirror.runMode(code, spec, el);
115 118 callback(null, el.innerHTML);
116 119 } catch (err) {
117 120 console.log("Failed to highlight " + lang + " code", err);
118 121 callback(err, code);
119 122 }
120 123 }, function (err) {
121 124 console.log("No CodeMirror mode: " + lang);
122 125 callback(err, code);
123 126 });
124 127 }
125 128 });
126 129 }
127 130
128 131 this.element = $(selector);
129 132 this.element.scroll();
130 133 this.element.data("notebook", this);
131 134 this.next_prompt_number = 1;
132 135 this.session = null;
133 136 this.kernel = null;
134 137 this.clipboard = null;
135 138 this.undelete_backup = null;
136 139 this.undelete_index = null;
137 140 this.undelete_below = false;
138 141 this.paste_enabled = false;
139 142 this.writable = false;
140 143 // It is important to start out in command mode to match the intial mode
141 144 // of the KeyboardManager.
142 145 this.mode = 'command';
143 146 this.set_dirty(false);
144 147 this.metadata = {};
145 148 this._checkpoint_after_save = false;
146 149 this.last_checkpoint = null;
147 150 this.checkpoints = [];
148 151 this.autosave_interval = 0;
149 152 this.autosave_timer = null;
150 153 // autosave *at most* every two minutes
151 154 this.minimum_autosave_interval = 120000;
152 155 this.notebook_name_blacklist_re = /[\/\\:]/;
153 156 this.nbformat = 4; // Increment this when changing the nbformat
154 157 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
155 158 this.codemirror_mode = 'ipython';
156 159 this.create_elements();
157 160 this.bind_events();
158 161 this.kernel_selector = null;
159 162 this.dirty = null;
160 163 this.trusted = null;
161 164 this._fully_loaded = false;
162 165
163 166 // Trigger cell toolbar registration.
164 167 default_celltoolbar.register(this);
165 168 rawcell_celltoolbar.register(this);
166 169 slideshow_celltoolbar.register(this);
167 170
168 171 // prevent assign to miss-typed properties.
169 172 Object.seal(this);
170 173 };
171 174
172 175 Notebook.options_default = {
173 176 // can be any cell type, or the special values of
174 177 // 'above', 'below', or 'selected' to get the value from another cell.
175 Notebook: {
176 default_cell_type: 'code'
177 }
178 default_cell_type: 'code'
178 179 };
179 180
180 181 /**
181 182 * Create an HTML and CSS representation of the notebook.
182 183 */
183 184 Notebook.prototype.create_elements = function () {
184 185 var that = this;
185 186 this.element.attr('tabindex','-1');
186 187 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
187 188 // We add this end_space div to the end of the notebook div to:
188 189 // i) provide a margin between the last cell and the end of the notebook
189 190 // ii) to prevent the div from scrolling up when the last cell is being
190 191 // edited, but is too low on the page, which browsers will do automatically.
191 192 var end_space = $('<div/>').addClass('end_space');
192 193 end_space.dblclick(function (e) {
193 194 var ncells = that.ncells();
194 195 that.insert_cell_below('code',ncells-1);
195 196 });
196 197 this.element.append(this.container);
197 198 this.container.append(end_space);
198 199 };
199 200
200 201 /**
201 202 * Bind JavaScript events: key presses and custom IPython events.
202 203 */
203 204 Notebook.prototype.bind_events = function () {
204 205 var that = this;
205 206
206 207 this.events.on('set_next_input.Notebook', function (event, data) {
207 208 if (data.replace) {
208 209 data.cell.set_text(data.text);
209 210 data.cell.clear_output();
210 211 } else {
211 212 var index = that.find_cell_index(data.cell);
212 213 var new_cell = that.insert_cell_below('code',index);
213 214 new_cell.set_text(data.text);
214 215 }
215 216 that.dirty = true;
216 217 });
217 218
218 219 this.events.on('unrecognized_cell.Cell', function () {
219 220 that.warn_nbformat_minor();
220 221 });
221 222
222 223 this.events.on('unrecognized_output.OutputArea', function () {
223 224 that.warn_nbformat_minor();
224 225 });
225 226
226 227 this.events.on('set_dirty.Notebook', function (event, data) {
227 228 that.dirty = data.value;
228 229 });
229 230
230 231 this.events.on('trust_changed.Notebook', function (event, trusted) {
231 232 that.trusted = trusted;
232 233 });
233 234
234 235 this.events.on('select.Cell', function (event, data) {
235 236 var index = that.find_cell_index(data.cell);
236 237 that.select(index);
237 238 });
238 239
239 240 this.events.on('edit_mode.Cell', function (event, data) {
240 241 that.handle_edit_mode(data.cell);
241 242 });
242 243
243 244 this.events.on('command_mode.Cell', function (event, data) {
244 245 that.handle_command_mode(data.cell);
245 246 });
246 247
247 248 this.events.on('spec_changed.Kernel', function(event, data) {
248 249 that.metadata.kernelspec =
249 250 {name: data.name, display_name: data.display_name};
250 251 });
251 252
252 253 this.events.on('kernel_ready.Kernel', function(event, data) {
253 254 var kinfo = data.kernel.info_reply;
254 255 var langinfo = kinfo.language_info || {};
255 256 that.metadata.language_info = langinfo;
256 257 // Mode 'null' should be plain, unhighlighted text.
257 258 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
258 259 that.set_codemirror_mode(cm_mode);
259 260 });
260 261
261 262 var collapse_time = function (time) {
262 263 var app_height = $('#ipython-main-app').height(); // content height
263 264 var splitter_height = $('div#pager_splitter').outerHeight(true);
264 265 var new_height = app_height - splitter_height;
265 266 that.element.animate({height : new_height + 'px'}, time);
266 267 };
267 268
268 269 this.element.bind('collapse_pager', function (event, extrap) {
269 270 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
270 271 collapse_time(time);
271 272 });
272 273
273 274 var expand_time = function (time) {
274 275 var app_height = $('#ipython-main-app').height(); // content height
275 276 var splitter_height = $('div#pager_splitter').outerHeight(true);
276 277 var pager_height = $('div#pager').outerHeight(true);
277 278 var new_height = app_height - pager_height - splitter_height;
278 279 that.element.animate({height : new_height + 'px'}, time);
279 280 };
280 281
281 282 this.element.bind('expand_pager', function (event, extrap) {
282 283 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
283 284 expand_time(time);
284 285 });
285 286
286 287 // Firefox 22 broke $(window).on("beforeunload")
287 288 // I'm not sure why or how.
288 289 window.onbeforeunload = function (e) {
289 290 // TODO: Make killing the kernel configurable.
290 291 var kill_kernel = false;
291 292 if (kill_kernel) {
292 293 that.session.delete();
293 294 }
294 295 // if we are autosaving, trigger an autosave on nav-away.
295 296 // still warn, because if we don't the autosave may fail.
296 297 if (that.dirty) {
297 298 if ( that.autosave_interval ) {
298 299 // schedule autosave in a timeout
299 300 // this gives you a chance to forcefully discard changes
300 301 // by reloading the page if you *really* want to.
301 302 // the timer doesn't start until you *dismiss* the dialog.
302 303 setTimeout(function () {
303 304 if (that.dirty) {
304 305 that.save_notebook();
305 306 }
306 307 }, 1000);
307 308 return "Autosave in progress, latest changes may be lost.";
308 309 } else {
309 310 return "Unsaved changes will be lost.";
310 311 }
311 312 }
312 313 // Null is the *only* return value that will make the browser not
313 314 // pop up the "don't leave" dialog.
314 315 return null;
315 316 };
316 317 };
317 318
318 319 /**
319 320 * Trigger a warning dialog about missing functionality from newer minor versions
320 321 */
321 322 Notebook.prototype.warn_nbformat_minor = function (event) {
322 323 var v = 'v' + this.nbformat + '.';
323 324 var orig_vs = v + this.nbformat_minor;
324 325 var this_vs = v + this.current_nbformat_minor;
325 326 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
326 327 this_vs + ". You can still work with this notebook, but cell and output types " +
327 328 "introduced in later notebook versions will not be available.";
328 329
329 330 dialog.modal({
330 331 notebook: this,
331 332 keyboard_manager: this.keyboard_manager,
332 333 title : "Newer Notebook",
333 334 body : msg,
334 335 buttons : {
335 336 OK : {
336 337 "class" : "btn-danger"
337 338 }
338 339 }
339 340 });
340 341 };
341 342
342 343 /**
343 344 * Set the dirty flag, and trigger the set_dirty.Notebook event
344 345 */
345 346 Notebook.prototype.set_dirty = function (value) {
346 347 if (value === undefined) {
347 348 value = true;
348 349 }
349 350 if (this.dirty == value) {
350 351 return;
351 352 }
352 353 this.events.trigger('set_dirty.Notebook', {value: value});
353 354 };
354 355
355 356 /**
356 357 * Scroll the top of the page to a given cell.
357 358 *
358 359 * @param {integer} index - An index of the cell to view
359 360 * @param {integer} time - Animation time in milliseconds
360 361 * @return {integer} Pixel offset from the top of the container
361 362 */
362 363 Notebook.prototype.scroll_to_cell = function (index, time) {
363 364 var cells = this.get_cells();
364 365 time = time || 0;
365 366 index = Math.min(cells.length-1,index);
366 367 index = Math.max(0 ,index);
367 368 var scroll_value = cells[index].element.position().top-cells[0].element.position().top ;
368 369 this.element.animate({scrollTop:scroll_value}, time);
369 370 return scroll_value;
370 371 };
371 372
372 373 /**
373 374 * Scroll to the bottom of the page.
374 375 */
375 376 Notebook.prototype.scroll_to_bottom = function () {
376 377 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
377 378 };
378 379
379 380 /**
380 381 * Scroll to the top of the page.
381 382 */
382 383 Notebook.prototype.scroll_to_top = function () {
383 384 this.element.animate({scrollTop:0}, 0);
384 385 };
385 386
386 387 // Edit Notebook metadata
387 388
388 389 /**
389 390 * Display a dialog that allows the user to edit the Notebook's metadata.
390 391 */
391 392 Notebook.prototype.edit_metadata = function () {
392 393 var that = this;
393 394 dialog.edit_metadata({
394 395 md: this.metadata,
395 396 callback: function (md) {
396 397 that.metadata = md;
397 398 },
398 399 name: 'Notebook',
399 400 notebook: this,
400 401 keyboard_manager: this.keyboard_manager});
401 402 };
402 403
403 404 // Cell indexing, retrieval, etc.
404 405
405 406 /**
406 407 * Get all cell elements in the notebook.
407 408 *
408 409 * @return {jQuery} A selector of all cell elements
409 410 */
410 411 Notebook.prototype.get_cell_elements = function () {
411 412 return this.container.find(".cell").not('.cell .cell');
412 413 };
413 414
414 415 /**
415 416 * Get a particular cell element.
416 417 *
417 418 * @param {integer} index An index of a cell to select
418 419 * @return {jQuery} A selector of the given cell.
419 420 */
420 421 Notebook.prototype.get_cell_element = function (index) {
421 422 var result = null;
422 423 var e = this.get_cell_elements().eq(index);
423 424 if (e.length !== 0) {
424 425 result = e;
425 426 }
426 427 return result;
427 428 };
428 429
429 430 /**
430 431 * Try to get a particular cell by msg_id.
431 432 *
432 433 * @param {string} msg_id A message UUID
433 434 * @return {Cell} Cell or null if no cell was found.
434 435 */
435 436 Notebook.prototype.get_msg_cell = function (msg_id) {
436 437 return codecell.CodeCell.msg_cells[msg_id] || null;
437 438 };
438 439
439 440 /**
440 441 * Count the cells in this notebook.
441 442 *
442 443 * @return {integer} The number of cells in this notebook
443 444 */
444 445 Notebook.prototype.ncells = function () {
445 446 return this.get_cell_elements().length;
446 447 };
447 448
448 449 /**
449 450 * Get all Cell objects in this notebook.
450 451 *
451 452 * @return {Array} This notebook's Cell objects
452 453 */
453 454 Notebook.prototype.get_cells = function () {
454 455 // TODO: we are often calling cells as cells()[i], which we should optimize
455 456 // to cells(i) or a new method.
456 457 return this.get_cell_elements().toArray().map(function (e) {
457 458 return $(e).data("cell");
458 459 });
459 460 };
460 461
461 462 /**
462 463 * Get a Cell objects from this notebook.
463 464 *
464 465 * @param {integer} index - An index of a cell to retrieve
465 466 * @return {Cell} Cell or null if no cell was found.
466 467 */
467 468 Notebook.prototype.get_cell = function (index) {
468 469 var result = null;
469 470 var ce = this.get_cell_element(index);
470 471 if (ce !== null) {
471 472 result = ce.data('cell');
472 473 }
473 474 return result;
474 475 };
475 476
476 477 /**
477 478 * Get the cell below a given cell.
478 479 *
479 480 * @param {Cell} cell
480 481 * @return {Cell} the next cell or null if no cell was found.
481 482 */
482 483 Notebook.prototype.get_next_cell = function (cell) {
483 484 var result = null;
484 485 var index = this.find_cell_index(cell);
485 486 if (this.is_valid_cell_index(index+1)) {
486 487 result = this.get_cell(index+1);
487 488 }
488 489 return result;
489 490 };
490 491
491 492 /**
492 493 * Get the cell above a given cell.
493 494 *
494 495 * @param {Cell} cell
495 496 * @return {Cell} The previous cell or null if no cell was found.
496 497 */
497 498 Notebook.prototype.get_prev_cell = function (cell) {
498 499 var result = null;
499 500 var index = this.find_cell_index(cell);
500 501 if (index !== null && index > 0) {
501 502 result = this.get_cell(index-1);
502 503 }
503 504 return result;
504 505 };
505 506
506 507 /**
507 508 * Get the numeric index of a given cell.
508 509 *
509 510 * @param {Cell} cell
510 511 * @return {integer} The cell's numeric index or null if no cell was found.
511 512 */
512 513 Notebook.prototype.find_cell_index = function (cell) {
513 514 var result = null;
514 515 this.get_cell_elements().filter(function (index) {
515 516 if ($(this).data("cell") === cell) {
516 517 result = index;
517 518 }
518 519 });
519 520 return result;
520 521 };
521 522
522 523 /**
523 524 * Return given index if defined, or the selected index if not.
524 525 *
525 526 * @param {integer} [index] - A cell's index
526 527 * @return {integer} cell index
527 528 */
528 529 Notebook.prototype.index_or_selected = function (index) {
529 530 var i;
530 531 if (index === undefined || index === null) {
531 532 i = this.get_selected_index();
532 533 if (i === null) {
533 534 i = 0;
534 535 }
535 536 } else {
536 537 i = index;
537 538 }
538 539 return i;
539 540 };
540 541
541 542 /**
542 543 * Get the currently selected cell.
543 544 *
544 545 * @return {Cell} The selected cell
545 546 */
546 547 Notebook.prototype.get_selected_cell = function () {
547 548 var index = this.get_selected_index();
548 549 return this.get_cell(index);
549 550 };
550 551
551 552 /**
552 553 * Check whether a cell index is valid.
553 554 *
554 555 * @param {integer} index - A cell index
555 556 * @return True if the index is valid, false otherwise
556 557 */
557 558 Notebook.prototype.is_valid_cell_index = function (index) {
558 559 if (index !== null && index >= 0 && index < this.ncells()) {
559 560 return true;
560 561 } else {
561 562 return false;
562 563 }
563 564 };
564 565
565 566 /**
566 567 * Get the index of the currently selected cell.
567 568 *
568 569 * @return {integer} The selected cell's numeric index
569 570 */
570 571 Notebook.prototype.get_selected_index = function () {
571 572 var result = null;
572 573 this.get_cell_elements().filter(function (index) {
573 574 if ($(this).data("cell").selected === true) {
574 575 result = index;
575 576 }
576 577 });
577 578 return result;
578 579 };
579 580
580 581
581 582 // Cell selection.
582 583
583 584 /**
584 585 * Programmatically select a cell.
585 586 *
586 587 * @param {integer} index - A cell's index
587 588 * @return {Notebook} This notebook
588 589 */
589 590 Notebook.prototype.select = function (index) {
590 591 if (this.is_valid_cell_index(index)) {
591 592 var sindex = this.get_selected_index();
592 593 if (sindex !== null && index !== sindex) {
593 594 // If we are about to select a different cell, make sure we are
594 595 // first in command mode.
595 596 if (this.mode !== 'command') {
596 597 this.command_mode();
597 598 }
598 599 this.get_cell(sindex).unselect();
599 600 }
600 601 var cell = this.get_cell(index);
601 602 cell.select();
602 603 if (cell.cell_type === 'heading') {
603 604 this.events.trigger('selected_cell_type_changed.Notebook',
604 605 {'cell_type':cell.cell_type,level:cell.level}
605 606 );
606 607 } else {
607 608 this.events.trigger('selected_cell_type_changed.Notebook',
608 609 {'cell_type':cell.cell_type}
609 610 );
610 611 }
611 612 }
612 613 return this;
613 614 };
614 615
615 616 /**
616 617 * Programmatically select the next cell.
617 618 *
618 619 * @return {Notebook} This notebook
619 620 */
620 621 Notebook.prototype.select_next = function () {
621 622 var index = this.get_selected_index();
622 623 this.select(index+1);
623 624 return this;
624 625 };
625 626
626 627 /**
627 628 * Programmatically select the previous cell.
628 629 *
629 630 * @return {Notebook} This notebook
630 631 */
631 632 Notebook.prototype.select_prev = function () {
632 633 var index = this.get_selected_index();
633 634 this.select(index-1);
634 635 return this;
635 636 };
636 637
637 638
638 639 // Edit/Command mode
639 640
640 641 /**
641 642 * Gets the index of the cell that is in edit mode.
642 643 *
643 644 * @return {integer} index
644 645 */
645 646 Notebook.prototype.get_edit_index = function () {
646 647 var result = null;
647 648 this.get_cell_elements().filter(function (index) {
648 649 if ($(this).data("cell").mode === 'edit') {
649 650 result = index;
650 651 }
651 652 });
652 653 return result;
653 654 };
654 655
655 656 /**
656 657 * Handle when a a cell blurs and the notebook should enter command mode.
657 658 *
658 659 * @param {Cell} [cell] - Cell to enter command mode on.
659 660 */
660 661 Notebook.prototype.handle_command_mode = function (cell) {
661 662 if (this.mode !== 'command') {
662 663 cell.command_mode();
663 664 this.mode = 'command';
664 665 this.events.trigger('command_mode.Notebook');
665 666 this.keyboard_manager.command_mode();
666 667 }
667 668 };
668 669
669 670 /**
670 671 * Make the notebook enter command mode.
671 672 */
672 673 Notebook.prototype.command_mode = function () {
673 674 var cell = this.get_cell(this.get_edit_index());
674 675 if (cell && this.mode !== 'command') {
675 676 // We don't call cell.command_mode, but rather call cell.focus_cell()
676 677 // which will blur and CM editor and trigger the call to
677 678 // handle_command_mode.
678 679 cell.focus_cell();
679 680 }
680 681 };
681 682
682 683 /**
683 684 * Handle when a cell fires it's edit_mode event.
684 685 *
685 686 * @param {Cell} [cell] Cell to enter edit mode on.
686 687 */
687 688 Notebook.prototype.handle_edit_mode = function (cell) {
688 689 if (cell && this.mode !== 'edit') {
689 690 cell.edit_mode();
690 691 this.mode = 'edit';
691 692 this.events.trigger('edit_mode.Notebook');
692 693 this.keyboard_manager.edit_mode();
693 694 }
694 695 };
695 696
696 697 /**
697 698 * Make a cell enter edit mode.
698 699 */
699 700 Notebook.prototype.edit_mode = function () {
700 701 var cell = this.get_selected_cell();
701 702 if (cell && this.mode !== 'edit') {
702 703 cell.unrender();
703 704 cell.focus_editor();
704 705 }
705 706 };
706 707
707 708 /**
708 709 * Focus the currently selected cell.
709 710 */
710 711 Notebook.prototype.focus_cell = function () {
711 712 var cell = this.get_selected_cell();
712 713 if (cell === null) {return;} // No cell is selected
713 714 cell.focus_cell();
714 715 };
715 716
716 717 // Cell movement
717 718
718 719 /**
719 720 * Move given (or selected) cell up and select it.
720 721 *
721 722 * @param {integer} [index] - cell index
722 723 * @return {Notebook} This notebook
723 724 */
724 725 Notebook.prototype.move_cell_up = function (index) {
725 726 var i = this.index_or_selected(index);
726 727 if (this.is_valid_cell_index(i) && i > 0) {
727 728 var pivot = this.get_cell_element(i-1);
728 729 var tomove = this.get_cell_element(i);
729 730 if (pivot !== null && tomove !== null) {
730 731 tomove.detach();
731 732 pivot.before(tomove);
732 733 this.select(i-1);
733 734 var cell = this.get_selected_cell();
734 735 cell.focus_cell();
735 736 }
736 737 this.set_dirty(true);
737 738 }
738 739 return this;
739 740 };
740 741
741 742
742 743 /**
743 744 * Move given (or selected) cell down and select it.
744 745 *
745 746 * @param {integer} [index] - cell index
746 747 * @return {Notebook} This notebook
747 748 */
748 749 Notebook.prototype.move_cell_down = function (index) {
749 750 var i = this.index_or_selected(index);
750 751 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
751 752 var pivot = this.get_cell_element(i+1);
752 753 var tomove = this.get_cell_element(i);
753 754 if (pivot !== null && tomove !== null) {
754 755 tomove.detach();
755 756 pivot.after(tomove);
756 757 this.select(i+1);
757 758 var cell = this.get_selected_cell();
758 759 cell.focus_cell();
759 760 }
760 761 }
761 762 this.set_dirty();
762 763 return this;
763 764 };
764 765
765 766
766 767 // Insertion, deletion.
767 768
768 769 /**
769 770 * Delete a cell from the notebook.
770 771 *
771 772 * @param {integer} [index] - cell's numeric index
772 773 * @return {Notebook} This notebook
773 774 */
774 775 Notebook.prototype.delete_cell = function (index) {
775 776 var i = this.index_or_selected(index);
776 777 var cell = this.get_cell(i);
777 778 if (!cell.is_deletable()) {
778 779 return this;
779 780 }
780 781
781 782 this.undelete_backup = cell.toJSON();
782 783 $('#undelete_cell').removeClass('disabled');
783 784 if (this.is_valid_cell_index(i)) {
784 785 var old_ncells = this.ncells();
785 786 var ce = this.get_cell_element(i);
786 787 ce.remove();
787 788 if (i === 0) {
788 789 // Always make sure we have at least one cell.
789 790 if (old_ncells === 1) {
790 791 this.insert_cell_below('code');
791 792 }
792 793 this.select(0);
793 794 this.undelete_index = 0;
794 795 this.undelete_below = false;
795 796 } else if (i === old_ncells-1 && i !== 0) {
796 797 this.select(i-1);
797 798 this.undelete_index = i - 1;
798 799 this.undelete_below = true;
799 800 } else {
800 801 this.select(i);
801 802 this.undelete_index = i;
802 803 this.undelete_below = false;
803 804 }
804 805 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
805 806 this.set_dirty(true);
806 807 }
807 808 return this;
808 809 };
809 810
810 811 /**
811 812 * Restore the most recently deleted cell.
812 813 */
813 814 Notebook.prototype.undelete_cell = function() {
814 815 if (this.undelete_backup !== null && this.undelete_index !== null) {
815 816 var current_index = this.get_selected_index();
816 817 if (this.undelete_index < current_index) {
817 818 current_index = current_index + 1;
818 819 }
819 820 if (this.undelete_index >= this.ncells()) {
820 821 this.select(this.ncells() - 1);
821 822 }
822 823 else {
823 824 this.select(this.undelete_index);
824 825 }
825 826 var cell_data = this.undelete_backup;
826 827 var new_cell = null;
827 828 if (this.undelete_below) {
828 829 new_cell = this.insert_cell_below(cell_data.cell_type);
829 830 } else {
830 831 new_cell = this.insert_cell_above(cell_data.cell_type);
831 832 }
832 833 new_cell.fromJSON(cell_data);
833 834 if (this.undelete_below) {
834 835 this.select(current_index+1);
835 836 } else {
836 837 this.select(current_index);
837 838 }
838 839 this.undelete_backup = null;
839 840 this.undelete_index = null;
840 841 }
841 842 $('#undelete_cell').addClass('disabled');
842 843 };
843 844
844 845 /**
845 846 * Insert a cell so that after insertion the cell is at given index.
846 847 *
847 848 * If cell type is not provided, it will default to the type of the
848 849 * currently active cell.
849 850 *
850 851 * Similar to insert_above, but index parameter is mandatory.
851 852 *
852 853 * Index will be brought back into the accessible range [0,n].
853 854 *
854 855 * @param {string} [type] - in ['code','markdown', 'raw'], defaults to 'code'
855 856 * @param {integer} [index] - a valid index where to insert cell
856 857 * @return {Cell|null} created cell or null
857 858 */
858 859 Notebook.prototype.insert_cell_at_index = function(type, index){
859 860
860 861 var ncells = this.ncells();
861 862 index = Math.min(index, ncells);
862 863 index = Math.max(index, 0);
863 864 var cell = null;
864 type = type || this.default_cell_type;
865 type = type || this.class_config.get_sync('default_cell_type');
865 866 if (type === 'above') {
866 867 if (index > 0) {
867 868 type = this.get_cell(index-1).cell_type;
868 869 } else {
869 870 type = 'code';
870 871 }
871 872 } else if (type === 'below') {
872 873 if (index < ncells) {
873 874 type = this.get_cell(index).cell_type;
874 875 } else {
875 876 type = 'code';
876 877 }
877 878 } else if (type === 'selected') {
878 879 type = this.get_selected_cell().cell_type;
879 880 }
880 881
881 882 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
882 883 var cell_options = {
883 884 events: this.events,
884 885 config: this.config,
885 886 keyboard_manager: this.keyboard_manager,
886 887 notebook: this,
887 888 tooltip: this.tooltip
888 889 };
889 890 switch(type) {
890 891 case 'code':
891 892 cell = new codecell.CodeCell(this.kernel, cell_options);
892 893 cell.set_input_prompt();
893 894 break;
894 895 case 'markdown':
895 896 cell = new textcell.MarkdownCell(cell_options);
896 897 break;
897 898 case 'raw':
898 899 cell = new textcell.RawCell(cell_options);
899 900 break;
900 901 default:
901 902 console.log("Unrecognized cell type: ", type, cellmod);
902 903 cell = new cellmod.UnrecognizedCell(cell_options);
903 904 }
904 905
905 906 if(this._insert_element_at_index(cell.element,index)) {
906 907 cell.render();
907 908 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
908 909 cell.refresh();
909 910 // We used to select the cell after we refresh it, but there
910 911 // are now cases were this method is called where select is
911 912 // not appropriate. The selection logic should be handled by the
912 913 // caller of the the top level insert_cell methods.
913 914 this.set_dirty(true);
914 915 }
915 916 }
916 917 return cell;
917 918
918 919 };
919 920
920 921 /**
921 922 * Insert an element at given cell index.
922 923 *
923 924 * @param {HTMLElement} element - a cell element
924 925 * @param {integer} [index] - a valid index where to inser cell
925 926 * @returns {boolean} success
926 927 */
927 928 Notebook.prototype._insert_element_at_index = function(element, index){
928 929 if (element === undefined){
929 930 return false;
930 931 }
931 932
932 933 var ncells = this.ncells();
933 934
934 935 if (ncells === 0) {
935 936 // special case append if empty
936 937 this.element.find('div.end_space').before(element);
937 938 } else if ( ncells === index ) {
938 939 // special case append it the end, but not empty
939 940 this.get_cell_element(index-1).after(element);
940 941 } else if (this.is_valid_cell_index(index)) {
941 942 // otherwise always somewhere to append to
942 943 this.get_cell_element(index).before(element);
943 944 } else {
944 945 return false;
945 946 }
946 947
947 948 if (this.undelete_index !== null && index <= this.undelete_index) {
948 949 this.undelete_index = this.undelete_index + 1;
949 950 this.set_dirty(true);
950 951 }
951 952 return true;
952 953 };
953 954
954 955 /**
955 956 * Insert a cell of given type above given index, or at top
956 957 * of notebook if index smaller than 0.
957 958 *
958 959 * @param {string} [type] - cell type
959 960 * @param {integer} [index] - defaults to the currently selected cell
960 961 * @return {Cell|null} handle to created cell or null
961 962 */
962 963 Notebook.prototype.insert_cell_above = function (type, index) {
963 964 index = this.index_or_selected(index);
964 965 return this.insert_cell_at_index(type, index);
965 966 };
966 967
967 968 /**
968 969 * Insert a cell of given type below given index, or at bottom
969 970 * of notebook if index greater than number of cells
970 971 *
971 972 * @param {string} [type] - cell type
972 973 * @param {integer} [index] - defaults to the currently selected cell
973 974 * @return {Cell|null} handle to created cell or null
974 975 */
975 976 Notebook.prototype.insert_cell_below = function (type, index) {
976 977 index = this.index_or_selected(index);
977 978 return this.insert_cell_at_index(type, index+1);
978 979 };
979 980
980 981
981 982 /**
982 983 * Insert cell at end of notebook
983 984 *
984 985 * @param {string} type - cell type
985 986 * @return {Cell|null} handle to created cell or null
986 987 */
987 988 Notebook.prototype.insert_cell_at_bottom = function (type){
988 989 var len = this.ncells();
989 990 return this.insert_cell_below(type,len-1);
990 991 };
991 992
992 993 /**
993 994 * Turn a cell into a code cell.
994 995 *
995 996 * @param {integer} [index] - cell index
996 997 */
997 998 Notebook.prototype.to_code = function (index) {
998 999 var i = this.index_or_selected(index);
999 1000 if (this.is_valid_cell_index(i)) {
1000 1001 var source_cell = this.get_cell(i);
1001 1002 if (!(source_cell instanceof codecell.CodeCell)) {
1002 1003 var target_cell = this.insert_cell_below('code',i);
1003 1004 var text = source_cell.get_text();
1004 1005 if (text === source_cell.placeholder) {
1005 1006 text = '';
1006 1007 }
1007 1008 //metadata
1008 1009 target_cell.metadata = source_cell.metadata;
1009 1010
1010 1011 target_cell.set_text(text);
1011 1012 // make this value the starting point, so that we can only undo
1012 1013 // to this state, instead of a blank cell
1013 1014 target_cell.code_mirror.clearHistory();
1014 1015 source_cell.element.remove();
1015 1016 this.select(i);
1016 1017 var cursor = source_cell.code_mirror.getCursor();
1017 1018 target_cell.code_mirror.setCursor(cursor);
1018 1019 this.set_dirty(true);
1019 1020 }
1020 1021 }
1021 1022 };
1022 1023
1023 1024 /**
1024 1025 * Turn a cell into a Markdown cell.
1025 1026 *
1026 1027 * @param {integer} [index] - cell index
1027 1028 */
1028 1029 Notebook.prototype.to_markdown = function (index) {
1029 1030 var i = this.index_or_selected(index);
1030 1031 if (this.is_valid_cell_index(i)) {
1031 1032 var source_cell = this.get_cell(i);
1032 1033
1033 1034 if (!(source_cell instanceof textcell.MarkdownCell)) {
1034 1035 var target_cell = this.insert_cell_below('markdown',i);
1035 1036 var text = source_cell.get_text();
1036 1037
1037 1038 if (text === source_cell.placeholder) {
1038 1039 text = '';
1039 1040 }
1040 1041 // metadata
1041 1042 target_cell.metadata = source_cell.metadata;
1042 1043 // We must show the editor before setting its contents
1043 1044 target_cell.unrender();
1044 1045 target_cell.set_text(text);
1045 1046 // make this value the starting point, so that we can only undo
1046 1047 // to this state, instead of a blank cell
1047 1048 target_cell.code_mirror.clearHistory();
1048 1049 source_cell.element.remove();
1049 1050 this.select(i);
1050 1051 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1051 1052 target_cell.render();
1052 1053 }
1053 1054 var cursor = source_cell.code_mirror.getCursor();
1054 1055 target_cell.code_mirror.setCursor(cursor);
1055 1056 this.set_dirty(true);
1056 1057 }
1057 1058 }
1058 1059 };
1059 1060
1060 1061 /**
1061 1062 * Turn a cell into a raw text cell.
1062 1063 *
1063 1064 * @param {integer} [index] - cell index
1064 1065 */
1065 1066 Notebook.prototype.to_raw = function (index) {
1066 1067 var i = this.index_or_selected(index);
1067 1068 if (this.is_valid_cell_index(i)) {
1068 1069 var target_cell = null;
1069 1070 var source_cell = this.get_cell(i);
1070 1071
1071 1072 if (!(source_cell instanceof textcell.RawCell)) {
1072 1073 target_cell = this.insert_cell_below('raw',i);
1073 1074 var text = source_cell.get_text();
1074 1075 if (text === source_cell.placeholder) {
1075 1076 text = '';
1076 1077 }
1077 1078 //metadata
1078 1079 target_cell.metadata = source_cell.metadata;
1079 1080 // We must show the editor before setting its contents
1080 1081 target_cell.unrender();
1081 1082 target_cell.set_text(text);
1082 1083 // make this value the starting point, so that we can only undo
1083 1084 // to this state, instead of a blank cell
1084 1085 target_cell.code_mirror.clearHistory();
1085 1086 source_cell.element.remove();
1086 1087 this.select(i);
1087 1088 var cursor = source_cell.code_mirror.getCursor();
1088 1089 target_cell.code_mirror.setCursor(cursor);
1089 1090 this.set_dirty(true);
1090 1091 }
1091 1092 }
1092 1093 };
1093 1094
1094 1095 /**
1095 1096 * Warn about heading cell support removal.
1096 1097 */
1097 1098 Notebook.prototype._warn_heading = function () {
1098 1099 dialog.modal({
1099 1100 notebook: this,
1100 1101 keyboard_manager: this.keyboard_manager,
1101 1102 title : "Use markdown headings",
1102 1103 body : $("<p/>").text(
1103 1104 'IPython no longer uses special heading cells. ' +
1104 1105 'Instead, write your headings in Markdown cells using # characters:'
1105 1106 ).append($('<pre/>').text(
1106 1107 '## This is a level 2 heading'
1107 1108 )),
1108 1109 buttons : {
1109 1110 "OK" : {}
1110 1111 }
1111 1112 });
1112 1113 };
1113 1114
1114 1115 /**
1115 1116 * Turn a cell into a heading containing markdown cell.
1116 1117 *
1117 1118 * @param {integer} [index] - cell index
1118 1119 * @param {integer} [level] - heading level (e.g., 1 for h1)
1119 1120 */
1120 1121 Notebook.prototype.to_heading = function (index, level) {
1121 1122 this.to_markdown(index);
1122 1123 level = level || 1;
1123 1124 var i = this.index_or_selected(index);
1124 1125 if (this.is_valid_cell_index(i)) {
1125 1126 var cell = this.get_cell(i);
1126 1127 cell.set_heading_level(level);
1127 1128 this.set_dirty(true);
1128 1129 }
1129 1130 };
1130 1131
1131 1132
1132 1133 // Cut/Copy/Paste
1133 1134
1134 1135 /**
1135 1136 * Enable the UI elements for pasting cells.
1136 1137 */
1137 1138 Notebook.prototype.enable_paste = function () {
1138 1139 var that = this;
1139 1140 if (!this.paste_enabled) {
1140 1141 $('#paste_cell_replace').removeClass('disabled')
1141 1142 .on('click', function () {that.paste_cell_replace();});
1142 1143 $('#paste_cell_above').removeClass('disabled')
1143 1144 .on('click', function () {that.paste_cell_above();});
1144 1145 $('#paste_cell_below').removeClass('disabled')
1145 1146 .on('click', function () {that.paste_cell_below();});
1146 1147 this.paste_enabled = true;
1147 1148 }
1148 1149 };
1149 1150
1150 1151 /**
1151 1152 * Disable the UI elements for pasting cells.
1152 1153 */
1153 1154 Notebook.prototype.disable_paste = function () {
1154 1155 if (this.paste_enabled) {
1155 1156 $('#paste_cell_replace').addClass('disabled').off('click');
1156 1157 $('#paste_cell_above').addClass('disabled').off('click');
1157 1158 $('#paste_cell_below').addClass('disabled').off('click');
1158 1159 this.paste_enabled = false;
1159 1160 }
1160 1161 };
1161 1162
1162 1163 /**
1163 1164 * Cut a cell.
1164 1165 */
1165 1166 Notebook.prototype.cut_cell = function () {
1166 1167 this.copy_cell();
1167 1168 this.delete_cell();
1168 1169 };
1169 1170
1170 1171 /**
1171 1172 * Copy a cell.
1172 1173 */
1173 1174 Notebook.prototype.copy_cell = function () {
1174 1175 var cell = this.get_selected_cell();
1175 1176 this.clipboard = cell.toJSON();
1176 1177 // remove undeletable status from the copied cell
1177 1178 if (this.clipboard.metadata.deletable !== undefined) {
1178 1179 delete this.clipboard.metadata.deletable;
1179 1180 }
1180 1181 this.enable_paste();
1181 1182 };
1182 1183
1183 1184 /**
1184 1185 * Replace the selected cell with the cell in the clipboard.
1185 1186 */
1186 1187 Notebook.prototype.paste_cell_replace = function () {
1187 1188 if (this.clipboard !== null && this.paste_enabled) {
1188 1189 var cell_data = this.clipboard;
1189 1190 var new_cell = this.insert_cell_above(cell_data.cell_type);
1190 1191 new_cell.fromJSON(cell_data);
1191 1192 var old_cell = this.get_next_cell(new_cell);
1192 1193 this.delete_cell(this.find_cell_index(old_cell));
1193 1194 this.select(this.find_cell_index(new_cell));
1194 1195 }
1195 1196 };
1196 1197
1197 1198 /**
1198 1199 * Paste a cell from the clipboard above the selected cell.
1199 1200 */
1200 1201 Notebook.prototype.paste_cell_above = function () {
1201 1202 if (this.clipboard !== null && this.paste_enabled) {
1202 1203 var cell_data = this.clipboard;
1203 1204 var new_cell = this.insert_cell_above(cell_data.cell_type);
1204 1205 new_cell.fromJSON(cell_data);
1205 1206 new_cell.focus_cell();
1206 1207 }
1207 1208 };
1208 1209
1209 1210 /**
1210 1211 * Paste a cell from the clipboard below the selected cell.
1211 1212 */
1212 1213 Notebook.prototype.paste_cell_below = function () {
1213 1214 if (this.clipboard !== null && this.paste_enabled) {
1214 1215 var cell_data = this.clipboard;
1215 1216 var new_cell = this.insert_cell_below(cell_data.cell_type);
1216 1217 new_cell.fromJSON(cell_data);
1217 1218 new_cell.focus_cell();
1218 1219 }
1219 1220 };
1220 1221
1221 1222 // Split/merge
1222 1223
1223 1224 /**
1224 1225 * Split the selected cell into two cells.
1225 1226 */
1226 1227 Notebook.prototype.split_cell = function () {
1227 1228 var cell = this.get_selected_cell();
1228 1229 if (cell.is_splittable()) {
1229 1230 var texta = cell.get_pre_cursor();
1230 1231 var textb = cell.get_post_cursor();
1231 1232 cell.set_text(textb);
1232 1233 var new_cell = this.insert_cell_above(cell.cell_type);
1233 1234 // Unrender the new cell so we can call set_text.
1234 1235 new_cell.unrender();
1235 1236 new_cell.set_text(texta);
1236 1237 }
1237 1238 };
1238 1239
1239 1240 /**
1240 1241 * Merge the selected cell into the cell above it.
1241 1242 */
1242 1243 Notebook.prototype.merge_cell_above = function () {
1243 1244 var index = this.get_selected_index();
1244 1245 var cell = this.get_cell(index);
1245 1246 var render = cell.rendered;
1246 1247 if (!cell.is_mergeable()) {
1247 1248 return;
1248 1249 }
1249 1250 if (index > 0) {
1250 1251 var upper_cell = this.get_cell(index-1);
1251 1252 if (!upper_cell.is_mergeable()) {
1252 1253 return;
1253 1254 }
1254 1255 var upper_text = upper_cell.get_text();
1255 1256 var text = cell.get_text();
1256 1257 if (cell instanceof codecell.CodeCell) {
1257 1258 cell.set_text(upper_text+'\n'+text);
1258 1259 } else {
1259 1260 cell.unrender(); // Must unrender before we set_text.
1260 1261 cell.set_text(upper_text+'\n\n'+text);
1261 1262 if (render) {
1262 1263 // The rendered state of the final cell should match
1263 1264 // that of the original selected cell;
1264 1265 cell.render();
1265 1266 }
1266 1267 }
1267 1268 this.delete_cell(index-1);
1268 1269 this.select(this.find_cell_index(cell));
1269 1270 }
1270 1271 };
1271 1272
1272 1273 /**
1273 1274 * Merge the selected cell into the cell below it.
1274 1275 */
1275 1276 Notebook.prototype.merge_cell_below = function () {
1276 1277 var index = this.get_selected_index();
1277 1278 var cell = this.get_cell(index);
1278 1279 var render = cell.rendered;
1279 1280 if (!cell.is_mergeable()) {
1280 1281 return;
1281 1282 }
1282 1283 if (index < this.ncells()-1) {
1283 1284 var lower_cell = this.get_cell(index+1);
1284 1285 if (!lower_cell.is_mergeable()) {
1285 1286 return;
1286 1287 }
1287 1288 var lower_text = lower_cell.get_text();
1288 1289 var text = cell.get_text();
1289 1290 if (cell instanceof codecell.CodeCell) {
1290 1291 cell.set_text(text+'\n'+lower_text);
1291 1292 } else {
1292 1293 cell.unrender(); // Must unrender before we set_text.
1293 1294 cell.set_text(text+'\n\n'+lower_text);
1294 1295 if (render) {
1295 1296 // The rendered state of the final cell should match
1296 1297 // that of the original selected cell;
1297 1298 cell.render();
1298 1299 }
1299 1300 }
1300 1301 this.delete_cell(index+1);
1301 1302 this.select(this.find_cell_index(cell));
1302 1303 }
1303 1304 };
1304 1305
1305 1306
1306 1307 // Cell collapsing and output clearing
1307 1308
1308 1309 /**
1309 1310 * Hide a cell's output.
1310 1311 *
1311 1312 * @param {integer} index - cell index
1312 1313 */
1313 1314 Notebook.prototype.collapse_output = function (index) {
1314 1315 var i = this.index_or_selected(index);
1315 1316 var cell = this.get_cell(i);
1316 1317 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1317 1318 cell.collapse_output();
1318 1319 this.set_dirty(true);
1319 1320 }
1320 1321 };
1321 1322
1322 1323 /**
1323 1324 * Hide each code cell's output area.
1324 1325 */
1325 1326 Notebook.prototype.collapse_all_output = function () {
1326 1327 this.get_cells().map(function (cell, i) {
1327 1328 if (cell instanceof codecell.CodeCell) {
1328 1329 cell.collapse_output();
1329 1330 }
1330 1331 });
1331 1332 // this should not be set if the `collapse` key is removed from nbformat
1332 1333 this.set_dirty(true);
1333 1334 };
1334 1335
1335 1336 /**
1336 1337 * Show a cell's output.
1337 1338 *
1338 1339 * @param {integer} index - cell index
1339 1340 */
1340 1341 Notebook.prototype.expand_output = function (index) {
1341 1342 var i = this.index_or_selected(index);
1342 1343 var cell = this.get_cell(i);
1343 1344 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1344 1345 cell.expand_output();
1345 1346 this.set_dirty(true);
1346 1347 }
1347 1348 };
1348 1349
1349 1350 /**
1350 1351 * Expand each code cell's output area, and remove scrollbars.
1351 1352 */
1352 1353 Notebook.prototype.expand_all_output = function () {
1353 1354 this.get_cells().map(function (cell, i) {
1354 1355 if (cell instanceof codecell.CodeCell) {
1355 1356 cell.expand_output();
1356 1357 }
1357 1358 });
1358 1359 // this should not be set if the `collapse` key is removed from nbformat
1359 1360 this.set_dirty(true);
1360 1361 };
1361 1362
1362 1363 /**
1363 1364 * Clear the selected CodeCell's output area.
1364 1365 *
1365 1366 * @param {integer} index - cell index
1366 1367 */
1367 1368 Notebook.prototype.clear_output = function (index) {
1368 1369 var i = this.index_or_selected(index);
1369 1370 var cell = this.get_cell(i);
1370 1371 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1371 1372 cell.clear_output();
1372 1373 this.set_dirty(true);
1373 1374 }
1374 1375 };
1375 1376
1376 1377 /**
1377 1378 * Clear each code cell's output area.
1378 1379 */
1379 1380 Notebook.prototype.clear_all_output = function () {
1380 1381 this.get_cells().map(function (cell, i) {
1381 1382 if (cell instanceof codecell.CodeCell) {
1382 1383 cell.clear_output();
1383 1384 }
1384 1385 });
1385 1386 this.set_dirty(true);
1386 1387 };
1387 1388
1388 1389 /**
1389 1390 * Scroll the selected CodeCell's output area.
1390 1391 *
1391 1392 * @param {integer} index - cell index
1392 1393 */
1393 1394 Notebook.prototype.scroll_output = function (index) {
1394 1395 var i = this.index_or_selected(index);
1395 1396 var cell = this.get_cell(i);
1396 1397 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1397 1398 cell.scroll_output();
1398 1399 this.set_dirty(true);
1399 1400 }
1400 1401 };
1401 1402
1402 1403 /**
1403 1404 * Expand each code cell's output area and add a scrollbar for long output.
1404 1405 */
1405 1406 Notebook.prototype.scroll_all_output = function () {
1406 1407 this.get_cells().map(function (cell, i) {
1407 1408 if (cell instanceof codecell.CodeCell) {
1408 1409 cell.scroll_output();
1409 1410 }
1410 1411 });
1411 1412 // this should not be set if the `collapse` key is removed from nbformat
1412 1413 this.set_dirty(true);
1413 1414 };
1414 1415
1415 1416 /**
1416 1417 * Toggle whether a cell's output is collapsed or expanded.
1417 1418 *
1418 1419 * @param {integer} index - cell index
1419 1420 */
1420 1421 Notebook.prototype.toggle_output = function (index) {
1421 1422 var i = this.index_or_selected(index);
1422 1423 var cell = this.get_cell(i);
1423 1424 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1424 1425 cell.toggle_output();
1425 1426 this.set_dirty(true);
1426 1427 }
1427 1428 };
1428 1429
1429 1430 /**
1430 1431 * Toggle the output of all cells.
1431 1432 */
1432 1433 Notebook.prototype.toggle_all_output = function () {
1433 1434 this.get_cells().map(function (cell, i) {
1434 1435 if (cell instanceof codecell.CodeCell) {
1435 1436 cell.toggle_output();
1436 1437 }
1437 1438 });
1438 1439 // this should not be set if the `collapse` key is removed from nbformat
1439 1440 this.set_dirty(true);
1440 1441 };
1441 1442
1442 1443 /**
1443 1444 * Toggle a scrollbar for long cell outputs.
1444 1445 *
1445 1446 * @param {integer} index - cell index
1446 1447 */
1447 1448 Notebook.prototype.toggle_output_scroll = function (index) {
1448 1449 var i = this.index_or_selected(index);
1449 1450 var cell = this.get_cell(i);
1450 1451 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1451 1452 cell.toggle_output_scroll();
1452 1453 this.set_dirty(true);
1453 1454 }
1454 1455 };
1455 1456
1456 1457 /**
1457 1458 * Toggle the scrolling of long output on all cells.
1458 1459 */
1459 1460 Notebook.prototype.toggle_all_output_scroll = function () {
1460 1461 this.get_cells().map(function (cell, i) {
1461 1462 if (cell instanceof codecell.CodeCell) {
1462 1463 cell.toggle_output_scroll();
1463 1464 }
1464 1465 });
1465 1466 // this should not be set if the `collapse` key is removed from nbformat
1466 1467 this.set_dirty(true);
1467 1468 };
1468 1469
1469 1470 // Other cell functions: line numbers, ...
1470 1471
1471 1472 /**
1472 1473 * Toggle line numbers in the selected cell's input area.
1473 1474 */
1474 1475 Notebook.prototype.cell_toggle_line_numbers = function() {
1475 1476 this.get_selected_cell().toggle_line_numbers();
1476 1477 };
1477 1478
1478 1479 /**
1479 1480 * Set the codemirror mode for all code cells, including the default for
1480 1481 * new code cells.
1481 1482 */
1482 1483 Notebook.prototype.set_codemirror_mode = function(newmode){
1483 1484 if (newmode === this.codemirror_mode) {
1484 1485 return;
1485 1486 }
1486 1487 this.codemirror_mode = newmode;
1487 1488 codecell.CodeCell.options_default.cm_config.mode = newmode;
1488 1489
1489 1490 var that = this;
1490 1491 utils.requireCodeMirrorMode(newmode, function (spec) {
1491 1492 that.get_cells().map(function(cell, i) {
1492 1493 if (cell.cell_type === 'code'){
1493 1494 cell.code_mirror.setOption('mode', spec);
1494 1495 // This is currently redundant, because cm_config ends up as
1495 1496 // codemirror's own .options object, but I don't want to
1496 1497 // rely on that.
1497 1498 cell.cm_config.mode = spec;
1498 1499 }
1499 1500 });
1500 1501 });
1501 1502 };
1502 1503
1503 1504 // Session related things
1504 1505
1505 1506 /**
1506 1507 * Start a new session and set it on each code cell.
1507 1508 */
1508 1509 Notebook.prototype.start_session = function (kernel_name) {
1509 1510 if (this._session_starting) {
1510 1511 throw new session.SessionAlreadyStarting();
1511 1512 }
1512 1513 this._session_starting = true;
1513 1514
1514 1515 var options = {
1515 1516 base_url: this.base_url,
1516 1517 ws_url: this.ws_url,
1517 1518 notebook_path: this.notebook_path,
1518 1519 notebook_name: this.notebook_name,
1519 1520 kernel_name: kernel_name,
1520 1521 notebook: this
1521 1522 };
1522 1523
1523 1524 var success = $.proxy(this._session_started, this);
1524 1525 var failure = $.proxy(this._session_start_failed, this);
1525 1526
1526 1527 if (this.session !== null) {
1527 1528 this.session.restart(options, success, failure);
1528 1529 } else {
1529 1530 this.session = new session.Session(options);
1530 1531 this.session.start(success, failure);
1531 1532 }
1532 1533 };
1533 1534
1534 1535
1535 1536 /**
1536 1537 * Once a session is started, link the code cells to the kernel and pass the
1537 1538 * comm manager to the widget manager.
1538 1539 */
1539 1540 Notebook.prototype._session_started = function (){
1540 1541 this._session_starting = false;
1541 1542 this.kernel = this.session.kernel;
1542 1543 var ncells = this.ncells();
1543 1544 for (var i=0; i<ncells; i++) {
1544 1545 var cell = this.get_cell(i);
1545 1546 if (cell instanceof codecell.CodeCell) {
1546 1547 cell.set_kernel(this.session.kernel);
1547 1548 }
1548 1549 }
1549 1550 };
1550 1551
1551 1552 /**
1552 1553 * Called when the session fails to start.
1553 1554 */
1554 1555 Notebook.prototype._session_start_failed = function(jqxhr, status, error){
1555 1556 this._session_starting = false;
1556 1557 utils.log_ajax_error(jqxhr, status, error);
1557 1558 };
1558 1559
1559 1560 /**
1560 1561 * Prompt the user to restart the IPython kernel.
1561 1562 */
1562 1563 Notebook.prototype.restart_kernel = function () {
1563 1564 var that = this;
1564 1565 dialog.modal({
1565 1566 notebook: this,
1566 1567 keyboard_manager: this.keyboard_manager,
1567 1568 title : "Restart kernel or continue running?",
1568 1569 body : $("<p/>").text(
1569 1570 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1570 1571 ),
1571 1572 buttons : {
1572 1573 "Continue running" : {},
1573 1574 "Restart" : {
1574 1575 "class" : "btn-danger",
1575 1576 "click" : function() {
1576 1577 that.kernel.restart();
1577 1578 }
1578 1579 }
1579 1580 }
1580 1581 });
1581 1582 };
1582 1583
1583 1584 /**
1584 1585 * Execute or render cell outputs and go into command mode.
1585 1586 */
1586 1587 Notebook.prototype.execute_cell = function () {
1587 1588 // mode = shift, ctrl, alt
1588 1589 var cell = this.get_selected_cell();
1589 1590
1590 1591 cell.execute();
1591 1592 this.command_mode();
1592 1593 this.set_dirty(true);
1593 1594 };
1594 1595
1595 1596 /**
1596 1597 * Execute or render cell outputs and insert a new cell below.
1597 1598 */
1598 1599 Notebook.prototype.execute_cell_and_insert_below = function () {
1599 1600 var cell = this.get_selected_cell();
1600 1601 var cell_index = this.find_cell_index(cell);
1601 1602
1602 1603 cell.execute();
1603 1604
1604 1605 // If we are at the end always insert a new cell and return
1605 1606 if (cell_index === (this.ncells()-1)) {
1606 1607 this.command_mode();
1607 1608 this.insert_cell_below();
1608 1609 this.select(cell_index+1);
1609 1610 this.edit_mode();
1610 1611 this.scroll_to_bottom();
1611 1612 this.set_dirty(true);
1612 1613 return;
1613 1614 }
1614 1615
1615 1616 this.command_mode();
1616 1617 this.insert_cell_below();
1617 1618 this.select(cell_index+1);
1618 1619 this.edit_mode();
1619 1620 this.set_dirty(true);
1620 1621 };
1621 1622
1622 1623 /**
1623 1624 * Execute or render cell outputs and select the next cell.
1624 1625 */
1625 1626 Notebook.prototype.execute_cell_and_select_below = function () {
1626 1627
1627 1628 var cell = this.get_selected_cell();
1628 1629 var cell_index = this.find_cell_index(cell);
1629 1630
1630 1631 cell.execute();
1631 1632
1632 1633 // If we are at the end always insert a new cell and return
1633 1634 if (cell_index === (this.ncells()-1)) {
1634 1635 this.command_mode();
1635 1636 this.insert_cell_below();
1636 1637 this.select(cell_index+1);
1637 1638 this.edit_mode();
1638 1639 this.scroll_to_bottom();
1639 1640 this.set_dirty(true);
1640 1641 return;
1641 1642 }
1642 1643
1643 1644 this.command_mode();
1644 1645 this.select(cell_index+1);
1645 1646 this.focus_cell();
1646 1647 this.set_dirty(true);
1647 1648 };
1648 1649
1649 1650 /**
1650 1651 * Execute all cells below the selected cell.
1651 1652 */
1652 1653 Notebook.prototype.execute_cells_below = function () {
1653 1654 this.execute_cell_range(this.get_selected_index(), this.ncells());
1654 1655 this.scroll_to_bottom();
1655 1656 };
1656 1657
1657 1658 /**
1658 1659 * Execute all cells above the selected cell.
1659 1660 */
1660 1661 Notebook.prototype.execute_cells_above = function () {
1661 1662 this.execute_cell_range(0, this.get_selected_index());
1662 1663 };
1663 1664
1664 1665 /**
1665 1666 * Execute all cells.
1666 1667 */
1667 1668 Notebook.prototype.execute_all_cells = function () {
1668 1669 this.execute_cell_range(0, this.ncells());
1669 1670 this.scroll_to_bottom();
1670 1671 };
1671 1672
1672 1673 /**
1673 1674 * Execute a contiguous range of cells.
1674 1675 *
1675 1676 * @param {integer} start - index of the first cell to execute (inclusive)
1676 1677 * @param {integer} end - index of the last cell to execute (exclusive)
1677 1678 */
1678 1679 Notebook.prototype.execute_cell_range = function (start, end) {
1679 1680 this.command_mode();
1680 1681 for (var i=start; i<end; i++) {
1681 1682 this.select(i);
1682 1683 this.execute_cell();
1683 1684 }
1684 1685 };
1685 1686
1686 1687 // Persistance and loading
1687 1688
1688 1689 /**
1689 1690 * Getter method for this notebook's name.
1690 1691 *
1691 1692 * @return {string} This notebook's name (excluding file extension)
1692 1693 */
1693 1694 Notebook.prototype.get_notebook_name = function () {
1694 1695 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1695 1696 return nbname;
1696 1697 };
1697 1698
1698 1699 /**
1699 1700 * Setter method for this notebook's name.
1700 1701 *
1701 1702 * @param {string} name
1702 1703 */
1703 1704 Notebook.prototype.set_notebook_name = function (name) {
1704 1705 var parent = utils.url_path_split(this.notebook_path)[0];
1705 1706 this.notebook_name = name;
1706 1707 this.notebook_path = utils.url_path_join(parent, name);
1707 1708 };
1708 1709
1709 1710 /**
1710 1711 * Check that a notebook's name is valid.
1711 1712 *
1712 1713 * @param {string} nbname - A name for this notebook
1713 1714 * @return {boolean} True if the name is valid, false if invalid
1714 1715 */
1715 1716 Notebook.prototype.test_notebook_name = function (nbname) {
1716 1717 nbname = nbname || '';
1717 1718 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1718 1719 return true;
1719 1720 } else {
1720 1721 return false;
1721 1722 }
1722 1723 };
1723 1724
1724 1725 /**
1725 1726 * Load a notebook from JSON (.ipynb).
1726 1727 *
1727 1728 * @param {object} data - JSON representation of a notebook
1728 1729 */
1729 1730 Notebook.prototype.fromJSON = function (data) {
1730 1731
1731 1732 var content = data.content;
1732 1733 var ncells = this.ncells();
1733 1734 var i;
1734 1735 for (i=0; i<ncells; i++) {
1735 1736 // Always delete cell 0 as they get renumbered as they are deleted.
1736 1737 this.delete_cell(0);
1737 1738 }
1738 1739 // Save the metadata and name.
1739 1740 this.metadata = content.metadata;
1740 1741 this.notebook_name = data.name;
1741 1742 this.notebook_path = data.path;
1742 1743 var trusted = true;
1743 1744
1744 1745 // Trigger an event changing the kernel spec - this will set the default
1745 1746 // codemirror mode
1746 1747 if (this.metadata.kernelspec !== undefined) {
1747 1748 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1748 1749 }
1749 1750
1750 1751 // Set the codemirror mode from language_info metadata
1751 1752 if (this.metadata.language_info !== undefined) {
1752 1753 var langinfo = this.metadata.language_info;
1753 1754 // Mode 'null' should be plain, unhighlighted text.
1754 1755 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1755 1756 this.set_codemirror_mode(cm_mode);
1756 1757 }
1757 1758
1758 1759 var new_cells = content.cells;
1759 1760 ncells = new_cells.length;
1760 1761 var cell_data = null;
1761 1762 var new_cell = null;
1762 1763 for (i=0; i<ncells; i++) {
1763 1764 cell_data = new_cells[i];
1764 1765 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1765 1766 new_cell.fromJSON(cell_data);
1766 1767 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1767 1768 trusted = false;
1768 1769 }
1769 1770 }
1770 1771 if (trusted !== this.trusted) {
1771 1772 this.trusted = trusted;
1772 1773 this.events.trigger("trust_changed.Notebook", trusted);
1773 1774 }
1774 1775 };
1775 1776
1776 1777 /**
1777 1778 * Dump this notebook into a JSON-friendly object.
1778 1779 *
1779 1780 * @return {object} A JSON-friendly representation of this notebook.
1780 1781 */
1781 1782 Notebook.prototype.toJSON = function () {
1782 1783 // remove the conversion indicator, which only belongs in-memory
1783 1784 delete this.metadata.orig_nbformat;
1784 1785 delete this.metadata.orig_nbformat_minor;
1785 1786
1786 1787 var cells = this.get_cells();
1787 1788 var ncells = cells.length;
1788 1789 var cell_array = new Array(ncells);
1789 1790 var trusted = true;
1790 1791 for (var i=0; i<ncells; i++) {
1791 1792 var cell = cells[i];
1792 1793 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1793 1794 trusted = false;
1794 1795 }
1795 1796 cell_array[i] = cell.toJSON();
1796 1797 }
1797 1798 var data = {
1798 1799 cells: cell_array,
1799 1800 metadata: this.metadata,
1800 1801 nbformat: this.nbformat,
1801 1802 nbformat_minor: this.nbformat_minor
1802 1803 };
1803 1804 if (trusted != this.trusted) {
1804 1805 this.trusted = trusted;
1805 1806 this.events.trigger("trust_changed.Notebook", trusted);
1806 1807 }
1807 1808 return data;
1808 1809 };
1809 1810
1810 1811 /**
1811 1812 * Start an autosave timer which periodically saves the notebook.
1812 1813 *
1813 1814 * @param {integer} interval - the autosave interval in milliseconds
1814 1815 */
1815 1816 Notebook.prototype.set_autosave_interval = function (interval) {
1816 1817 var that = this;
1817 1818 // clear previous interval, so we don't get simultaneous timers
1818 1819 if (this.autosave_timer) {
1819 1820 clearInterval(this.autosave_timer);
1820 1821 }
1821 1822 if (!this.writable) {
1822 1823 // disable autosave if not writable
1823 1824 interval = 0;
1824 1825 }
1825 1826
1826 1827 this.autosave_interval = this.minimum_autosave_interval = interval;
1827 1828 if (interval) {
1828 1829 this.autosave_timer = setInterval(function() {
1829 1830 if (that.dirty) {
1830 1831 that.save_notebook();
1831 1832 }
1832 1833 }, interval);
1833 1834 this.events.trigger("autosave_enabled.Notebook", interval);
1834 1835 } else {
1835 1836 this.autosave_timer = null;
1836 1837 this.events.trigger("autosave_disabled.Notebook");
1837 1838 }
1838 1839 };
1839 1840
1840 1841 /**
1841 1842 * Save this notebook on the server. This becomes a notebook instance's
1842 1843 * .save_notebook method *after* the entire notebook has been loaded.
1843 1844 */
1844 1845 Notebook.prototype.save_notebook = function () {
1845 1846 if (!this._fully_loaded) {
1846 1847 this.events.trigger('notebook_save_failed.Notebook',
1847 1848 new Error("Load failed, save is disabled")
1848 1849 );
1849 1850 return;
1850 1851 } else if (!this.writable) {
1851 1852 this.events.trigger('notebook_save_failed.Notebook',
1852 1853 new Error("Notebook is read-only")
1853 1854 );
1854 1855 return;
1855 1856 }
1856 1857
1857 1858 // Trigger an event before save, which allows listeners to modify
1858 1859 // the notebook as needed.
1859 1860 this.events.trigger('before_save.Notebook');
1860 1861
1861 1862 // Create a JSON model to be sent to the server.
1862 1863 var model = {
1863 1864 type : "notebook",
1864 1865 content : this.toJSON()
1865 1866 };
1866 1867 // time the ajax call for autosave tuning purposes.
1867 1868 var start = new Date().getTime();
1868 1869
1869 1870 var that = this;
1870 1871 return this.contents.save(this.notebook_path, model).then(
1871 1872 $.proxy(this.save_notebook_success, this, start),
1872 1873 function (error) {
1873 1874 that.events.trigger('notebook_save_failed.Notebook', error);
1874 1875 }
1875 1876 );
1876 1877 };
1877 1878
1878 1879 /**
1879 1880 * Success callback for saving a notebook.
1880 1881 *
1881 1882 * @param {integer} start - Time when the save request start
1882 1883 * @param {object} data - JSON representation of a notebook
1883 1884 */
1884 1885 Notebook.prototype.save_notebook_success = function (start, data) {
1885 1886 this.set_dirty(false);
1886 1887 if (data.message) {
1887 1888 // save succeeded, but validation failed.
1888 1889 var body = $("<div>");
1889 1890 var title = "Notebook validation failed";
1890 1891
1891 1892 body.append($("<p>").text(
1892 1893 "The save operation succeeded," +
1893 1894 " but the notebook does not appear to be valid." +
1894 1895 " The validation error was:"
1895 1896 )).append($("<div>").addClass("validation-error").append(
1896 1897 $("<pre>").text(data.message)
1897 1898 ));
1898 1899 dialog.modal({
1899 1900 notebook: this,
1900 1901 keyboard_manager: this.keyboard_manager,
1901 1902 title: title,
1902 1903 body: body,
1903 1904 buttons : {
1904 1905 OK : {
1905 1906 "class" : "btn-primary"
1906 1907 }
1907 1908 }
1908 1909 });
1909 1910 }
1910 1911 this.events.trigger('notebook_saved.Notebook');
1911 1912 this._update_autosave_interval(start);
1912 1913 if (this._checkpoint_after_save) {
1913 1914 this.create_checkpoint();
1914 1915 this._checkpoint_after_save = false;
1915 1916 }
1916 1917 };
1917 1918
1918 1919 /**
1919 1920 * Update the autosave interval based on the duration of the last save.
1920 1921 *
1921 1922 * @param {integer} timestamp - when the save request started
1922 1923 */
1923 1924 Notebook.prototype._update_autosave_interval = function (start) {
1924 1925 var duration = (new Date().getTime() - start);
1925 1926 if (this.autosave_interval) {
1926 1927 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1927 1928 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1928 1929 // round to 10 seconds, otherwise we will be setting a new interval too often
1929 1930 interval = 10000 * Math.round(interval / 10000);
1930 1931 // set new interval, if it's changed
1931 1932 if (interval != this.autosave_interval) {
1932 1933 this.set_autosave_interval(interval);
1933 1934 }
1934 1935 }
1935 1936 };
1936 1937
1937 1938 /**
1938 1939 * Explicitly trust the output of this notebook.
1939 1940 */
1940 1941 Notebook.prototype.trust_notebook = function () {
1941 1942 var body = $("<div>").append($("<p>")
1942 1943 .text("A trusted IPython notebook may execute hidden malicious code ")
1943 1944 .append($("<strong>")
1944 1945 .append(
1945 1946 $("<em>").text("when you open it")
1946 1947 )
1947 1948 ).append(".").append(
1948 1949 " Selecting trust will immediately reload this notebook in a trusted state."
1949 1950 ).append(
1950 1951 " For more information, see the "
1951 1952 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1952 1953 .text("IPython security documentation")
1953 1954 ).append(".")
1954 1955 );
1955 1956
1956 1957 var nb = this;
1957 1958 dialog.modal({
1958 1959 notebook: this,
1959 1960 keyboard_manager: this.keyboard_manager,
1960 1961 title: "Trust this notebook?",
1961 1962 body: body,
1962 1963
1963 1964 buttons: {
1964 1965 Cancel : {},
1965 1966 Trust : {
1966 1967 class : "btn-danger",
1967 1968 click : function () {
1968 1969 var cells = nb.get_cells();
1969 1970 for (var i = 0; i < cells.length; i++) {
1970 1971 var cell = cells[i];
1971 1972 if (cell.cell_type == 'code') {
1972 1973 cell.output_area.trusted = true;
1973 1974 }
1974 1975 }
1975 1976 nb.events.on('notebook_saved.Notebook', function () {
1976 1977 window.location.reload();
1977 1978 });
1978 1979 nb.save_notebook();
1979 1980 }
1980 1981 }
1981 1982 }
1982 1983 });
1983 1984 };
1984 1985
1985 1986 /**
1986 1987 * Make a copy of the current notebook.
1987 1988 */
1988 1989 Notebook.prototype.copy_notebook = function () {
1989 1990 var that = this;
1990 1991 var base_url = this.base_url;
1991 1992 var w = window.open();
1992 1993 var parent = utils.url_path_split(this.notebook_path)[0];
1993 1994 this.contents.copy(this.notebook_path, parent).then(
1994 1995 function (data) {
1995 1996 w.location = utils.url_join_encode(
1996 1997 base_url, 'notebooks', data.path
1997 1998 );
1998 1999 },
1999 2000 function(error) {
2000 2001 w.close();
2001 2002 that.events.trigger('notebook_copy_failed', error);
2002 2003 }
2003 2004 );
2004 2005 };
2005 2006
2006 2007 /**
2007 2008 * Rename the notebook.
2008 2009 * @param {string} new_name
2009 2010 * @return {Promise} promise that resolves when the notebook is renamed.
2010 2011 */
2011 2012 Notebook.prototype.rename = function (new_name) {
2012 2013 if (!new_name.match(/\.ipynb$/)) {
2013 2014 new_name = new_name + ".ipynb";
2014 2015 }
2015 2016
2016 2017 var that = this;
2017 2018 var parent = utils.url_path_split(this.notebook_path)[0];
2018 2019 var new_path = utils.url_path_join(parent, new_name);
2019 2020 return this.contents.rename(this.notebook_path, new_path).then(
2020 2021 function (json) {
2021 2022 that.notebook_name = json.name;
2022 2023 that.notebook_path = json.path;
2023 2024 that.session.rename_notebook(json.path);
2024 2025 that.events.trigger('notebook_renamed.Notebook', json);
2025 2026 }
2026 2027 );
2027 2028 };
2028 2029
2029 2030 /**
2030 2031 * Delete this notebook
2031 2032 */
2032 2033 Notebook.prototype.delete = function () {
2033 2034 this.contents.delete(this.notebook_path);
2034 2035 };
2035 2036
2036 2037 /**
2037 2038 * Request a notebook's data from the server.
2038 2039 *
2039 2040 * @param {string} notebook_path - A notebook to load
2040 2041 */
2041 2042 Notebook.prototype.load_notebook = function (notebook_path) {
2042 2043 this.notebook_path = notebook_path;
2043 2044 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2044 2045 this.events.trigger('notebook_loading.Notebook');
2045 2046 this.contents.get(notebook_path, {type: 'notebook'}).then(
2046 2047 $.proxy(this.load_notebook_success, this),
2047 2048 $.proxy(this.load_notebook_error, this)
2048 2049 );
2049 2050 };
2050 2051
2051 2052 /**
2052 2053 * Success callback for loading a notebook from the server.
2053 2054 *
2054 2055 * Load notebook data from the JSON response.
2055 2056 *
2056 2057 * @param {object} data JSON representation of a notebook
2057 2058 */
2058 2059 Notebook.prototype.load_notebook_success = function (data) {
2059 2060 var failed, msg;
2060 2061 try {
2061 2062 this.fromJSON(data);
2062 2063 } catch (e) {
2063 2064 failed = e;
2064 2065 console.log("Notebook failed to load from JSON:", e);
2065 2066 }
2066 2067 if (failed || data.message) {
2067 2068 // *either* fromJSON failed or validation failed
2068 2069 var body = $("<div>");
2069 2070 var title;
2070 2071 if (failed) {
2071 2072 title = "Notebook failed to load";
2072 2073 body.append($("<p>").text(
2073 2074 "The error was: "
2074 2075 )).append($("<div>").addClass("js-error").text(
2075 2076 failed.toString()
2076 2077 )).append($("<p>").text(
2077 2078 "See the error console for details."
2078 2079 ));
2079 2080 } else {
2080 2081 title = "Notebook validation failed";
2081 2082 }
2082 2083
2083 2084 if (data.message) {
2084 2085 if (failed) {
2085 2086 msg = "The notebook also failed validation:";
2086 2087 } else {
2087 2088 msg = "An invalid notebook may not function properly." +
2088 2089 " The validation error was:";
2089 2090 }
2090 2091 body.append($("<p>").text(
2091 2092 msg
2092 2093 )).append($("<div>").addClass("validation-error").append(
2093 2094 $("<pre>").text(data.message)
2094 2095 ));
2095 2096 }
2096 2097
2097 2098 dialog.modal({
2098 2099 notebook: this,
2099 2100 keyboard_manager: this.keyboard_manager,
2100 2101 title: title,
2101 2102 body: body,
2102 2103 buttons : {
2103 2104 OK : {
2104 2105 "class" : "btn-primary"
2105 2106 }
2106 2107 }
2107 2108 });
2108 2109 }
2109 2110 if (this.ncells() === 0) {
2110 2111 this.insert_cell_below('code');
2111 2112 this.edit_mode(0);
2112 2113 } else {
2113 2114 this.select(0);
2114 2115 this.handle_command_mode(this.get_cell(0));
2115 2116 }
2116 2117 this.set_dirty(false);
2117 2118 this.scroll_to_top();
2118 2119 this.writable = data.writable || false;
2119 2120 var nbmodel = data.content;
2120 2121 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2121 2122 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2122 2123 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2123 2124 var src;
2124 2125 if (nbmodel.nbformat > orig_nbformat) {
2125 2126 src = " an older notebook format ";
2126 2127 } else {
2127 2128 src = " a newer notebook format ";
2128 2129 }
2129 2130
2130 2131 msg = "This notebook has been converted from" + src +
2131 2132 "(v"+orig_nbformat+") to the current notebook " +
2132 2133 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2133 2134 "current notebook format will be used.";
2134 2135
2135 2136 if (nbmodel.nbformat > orig_nbformat) {
2136 2137 msg += " Older versions of IPython may not be able to read the new format.";
2137 2138 } else {
2138 2139 msg += " Some features of the original notebook may not be available.";
2139 2140 }
2140 2141 msg += " To preserve the original version, close the " +
2141 2142 "notebook without saving it.";
2142 2143 dialog.modal({
2143 2144 notebook: this,
2144 2145 keyboard_manager: this.keyboard_manager,
2145 2146 title : "Notebook converted",
2146 2147 body : msg,
2147 2148 buttons : {
2148 2149 OK : {
2149 2150 class : "btn-primary"
2150 2151 }
2151 2152 }
2152 2153 });
2153 2154 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2154 2155 this.nbformat_minor = nbmodel.nbformat_minor;
2155 2156 }
2156 2157
2157 2158 // Create the session after the notebook is completely loaded to prevent
2158 2159 // code execution upon loading, which is a security risk.
2159 2160 if (this.session === null) {
2160 2161 var kernel_name;
2161 2162 if (this.metadata.kernelspec) {
2162 2163 var kernelspec = this.metadata.kernelspec || {};
2163 2164 kernel_name = kernelspec.name;
2164 2165 } else {
2165 2166 kernel_name = utils.get_url_param('kernel_name');
2166 2167 }
2167 2168 this.start_session(kernel_name);
2168 2169 }
2169 2170 // load our checkpoint list
2170 2171 this.list_checkpoints();
2171 2172
2172 2173 // load toolbar state
2173 2174 if (this.metadata.celltoolbar) {
2174 2175 celltoolbar.CellToolbar.global_show();
2175 2176 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2176 2177 } else {
2177 2178 celltoolbar.CellToolbar.global_hide();
2178 2179 }
2179 2180
2180 2181 if (!this.writable) {
2181 2182 this.set_autosave_interval(0);
2182 2183 this.events.trigger('notebook_read_only.Notebook');
2183 2184 }
2184 2185
2185 2186 // now that we're fully loaded, it is safe to restore save functionality
2186 2187 this._fully_loaded = true;
2187 2188 this.events.trigger('notebook_loaded.Notebook');
2188 2189 };
2189 2190
2190 2191 /**
2191 2192 * Failure callback for loading a notebook from the server.
2192 2193 *
2193 2194 * @param {Error} error
2194 2195 */
2195 2196 Notebook.prototype.load_notebook_error = function (error) {
2196 2197 this.events.trigger('notebook_load_failed.Notebook', error);
2197 2198 var msg;
2198 2199 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2199 2200 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2200 2201 msg = "An unknown error occurred while loading this notebook. " +
2201 2202 "This version can load notebook formats " +
2202 2203 "v" + this.nbformat + " or earlier. See the server log for details.";
2203 2204 } else {
2204 2205 msg = error.message;
2205 2206 }
2206 2207 dialog.modal({
2207 2208 notebook: this,
2208 2209 keyboard_manager: this.keyboard_manager,
2209 2210 title: "Error loading notebook",
2210 2211 body : msg,
2211 2212 buttons : {
2212 2213 "OK": {}
2213 2214 }
2214 2215 });
2215 2216 };
2216 2217
2217 2218 /********************* checkpoint-related ********************/
2218 2219
2219 2220 /**
2220 2221 * Save the notebook then immediately create a checkpoint.
2221 2222 */
2222 2223 Notebook.prototype.save_checkpoint = function () {
2223 2224 this._checkpoint_after_save = true;
2224 2225 this.save_notebook();
2225 2226 };
2226 2227
2227 2228 /**
2228 2229 * Add a checkpoint for this notebook.
2229 2230 */
2230 2231 Notebook.prototype.add_checkpoint = function (checkpoint) {
2231 2232 var found = false;
2232 2233 for (var i = 0; i < this.checkpoints.length; i++) {
2233 2234 var existing = this.checkpoints[i];
2234 2235 if (existing.id == checkpoint.id) {
2235 2236 found = true;
2236 2237 this.checkpoints[i] = checkpoint;
2237 2238 break;
2238 2239 }
2239 2240 }
2240 2241 if (!found) {
2241 2242 this.checkpoints.push(checkpoint);
2242 2243 }
2243 2244 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2244 2245 };
2245 2246
2246 2247 /**
2247 2248 * List checkpoints for this notebook.
2248 2249 */
2249 2250 Notebook.prototype.list_checkpoints = function () {
2250 2251 var that = this;
2251 2252 this.contents.list_checkpoints(this.notebook_path).then(
2252 2253 $.proxy(this.list_checkpoints_success, this),
2253 2254 function(error) {
2254 2255 that.events.trigger('list_checkpoints_failed.Notebook', error);
2255 2256 }
2256 2257 );
2257 2258 };
2258 2259
2259 2260 /**
2260 2261 * Success callback for listing checkpoints.
2261 2262 *
2262 2263 * @param {object} data - JSON representation of a checkpoint
2263 2264 */
2264 2265 Notebook.prototype.list_checkpoints_success = function (data) {
2265 2266 this.checkpoints = data;
2266 2267 if (data.length) {
2267 2268 this.last_checkpoint = data[data.length - 1];
2268 2269 } else {
2269 2270 this.last_checkpoint = null;
2270 2271 }
2271 2272 this.events.trigger('checkpoints_listed.Notebook', [data]);
2272 2273 };
2273 2274
2274 2275 /**
2275 2276 * Create a checkpoint of this notebook on the server from the most recent save.
2276 2277 */
2277 2278 Notebook.prototype.create_checkpoint = function () {
2278 2279 var that = this;
2279 2280 this.contents.create_checkpoint(this.notebook_path).then(
2280 2281 $.proxy(this.create_checkpoint_success, this),
2281 2282 function (error) {
2282 2283 that.events.trigger('checkpoint_failed.Notebook', error);
2283 2284 }
2284 2285 );
2285 2286 };
2286 2287
2287 2288 /**
2288 2289 * Success callback for creating a checkpoint.
2289 2290 *
2290 2291 * @param {object} data - JSON representation of a checkpoint
2291 2292 */
2292 2293 Notebook.prototype.create_checkpoint_success = function (data) {
2293 2294 this.add_checkpoint(data);
2294 2295 this.events.trigger('checkpoint_created.Notebook', data);
2295 2296 };
2296 2297
2297 2298 /**
2298 2299 * Display the restore checkpoint dialog
2299 2300 * @param {string} checkpoint ID
2300 2301 */
2301 2302 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2302 2303 var that = this;
2303 2304 checkpoint = checkpoint || this.last_checkpoint;
2304 2305 if ( ! checkpoint ) {
2305 2306 console.log("restore dialog, but no checkpoint to restore to!");
2306 2307 return;
2307 2308 }
2308 2309 var body = $('<div/>').append(
2309 2310 $('<p/>').addClass("p-space").text(
2310 2311 "Are you sure you want to revert the notebook to " +
2311 2312 "the latest checkpoint?"
2312 2313 ).append(
2313 2314 $("<strong/>").text(
2314 2315 " This cannot be undone."
2315 2316 )
2316 2317 )
2317 2318 ).append(
2318 2319 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2319 2320 ).append(
2320 2321 $('<p/>').addClass("p-space").text(
2321 2322 Date(checkpoint.last_modified)
2322 2323 ).css("text-align", "center")
2323 2324 );
2324 2325
2325 2326 dialog.modal({
2326 2327 notebook: this,
2327 2328 keyboard_manager: this.keyboard_manager,
2328 2329 title : "Revert notebook to checkpoint",
2329 2330 body : body,
2330 2331 buttons : {
2331 2332 Revert : {
2332 2333 class : "btn-danger",
2333 2334 click : function () {
2334 2335 that.restore_checkpoint(checkpoint.id);
2335 2336 }
2336 2337 },
2337 2338 Cancel : {}
2338 2339 }
2339 2340 });
2340 2341 };
2341 2342
2342 2343 /**
2343 2344 * Restore the notebook to a checkpoint state.
2344 2345 *
2345 2346 * @param {string} checkpoint ID
2346 2347 */
2347 2348 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2348 2349 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2349 2350 var that = this;
2350 2351 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2351 2352 $.proxy(this.restore_checkpoint_success, this),
2352 2353 function (error) {
2353 2354 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2354 2355 }
2355 2356 );
2356 2357 };
2357 2358
2358 2359 /**
2359 2360 * Success callback for restoring a notebook to a checkpoint.
2360 2361 */
2361 2362 Notebook.prototype.restore_checkpoint_success = function () {
2362 2363 this.events.trigger('checkpoint_restored.Notebook');
2363 2364 this.load_notebook(this.notebook_path);
2364 2365 };
2365 2366
2366 2367 /**
2367 2368 * Delete a notebook checkpoint.
2368 2369 *
2369 2370 * @param {string} checkpoint ID
2370 2371 */
2371 2372 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2372 2373 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2373 2374 var that = this;
2374 2375 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2375 2376 $.proxy(this.delete_checkpoint_success, this),
2376 2377 function (error) {
2377 2378 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2378 2379 }
2379 2380 );
2380 2381 };
2381 2382
2382 2383 /**
2383 2384 * Success callback for deleting a notebook checkpoint.
2384 2385 */
2385 2386 Notebook.prototype.delete_checkpoint_success = function () {
2386 2387 this.events.trigger('checkpoint_deleted.Notebook');
2387 2388 this.load_notebook(this.notebook_path);
2388 2389 };
2389 2390
2390 2391
2391 2392 // For backwards compatability.
2392 2393 IPython.Notebook = Notebook;
2393 2394
2394 2395 return {'Notebook': Notebook};
2395 2396 });
@@ -1,352 +1,375 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'base/js/utils',
7 7 'jquery',
8 8 'notebook/js/cell',
9 9 'base/js/security',
10 'services/config',
10 11 'notebook/js/mathjaxutils',
11 12 'notebook/js/celltoolbar',
12 13 'components/marked/lib/marked',
13 14 'codemirror/lib/codemirror',
14 15 'codemirror/mode/gfm/gfm',
15 16 'notebook/js/codemirror-ipythongfm'
16 ], function(IPython,utils , $, cell, security, mathjaxutils, celltoolbar, marked, CodeMirror, gfm, ipgfm) {
17 ], function(IPython,
18 utils,
19 $,
20 cell,
21 security,
22 configmod,
23 mathjaxutils,
24 celltoolbar,
25 marked,
26 CodeMirror,
27 gfm,
28 ipgfm
29 ) {
17 30 "use strict";
18 31 var Cell = cell.Cell;
19 32
20 33 var TextCell = function (options) {
21 34 /**
22 35 * Constructor
23 36 *
24 37 * Construct a new TextCell, codemirror mode is by default 'htmlmixed',
25 38 * and cell type is 'text' cell start as not redered.
26 39 *
27 40 * Parameters:
28 41 * options: dictionary
29 42 * Dictionary of keyword arguments.
30 43 * events: $(Events) instance
31 44 * config: dictionary
32 45 * keyboard_manager: KeyboardManager instance
33 46 * notebook: Notebook instance
34 47 */
35 48 options = options || {};
36 49
37 50 // in all TextCell/Cell subclasses
38 51 // do not assign most of members here, just pass it down
39 52 // in the options dict potentially overwriting what you wish.
40 53 // they will be assigned in the base class.
41 54 this.notebook = options.notebook;
42 55 this.events = options.events;
43 56 this.config = options.config;
44 57
45 58 // we cannot put this as a class key as it has handle to "this".
46 59 var config = utils.mergeopt(TextCell, this.config);
47 60 Cell.apply(this, [{
48 61 config: config,
49 62 keyboard_manager: options.keyboard_manager,
50 63 events: this.events}]);
51 64
52 65 this.cell_type = this.cell_type || 'text';
53 66 mathjaxutils = mathjaxutils;
54 67 this.rendered = false;
55 68 };
56 69
57 70 TextCell.prototype = Object.create(Cell.prototype);
58 71
59 72 TextCell.options_default = {
60 73 cm_config : {
61 74 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
62 75 mode: 'htmlmixed',
63 76 lineWrapping : true,
64 77 }
65 78 };
66 79
67 80
68 81 /**
69 82 * Create the DOM element of the TextCell
70 83 * @method create_element
71 84 * @private
72 85 */
73 86 TextCell.prototype.create_element = function () {
74 87 Cell.prototype.create_element.apply(this, arguments);
75 88
76 89 var cell = $("<div>").addClass('cell text_cell');
77 90 cell.attr('tabindex','2');
78 91
79 92 var prompt = $('<div/>').addClass('prompt input_prompt');
80 93 cell.append(prompt);
81 94 var inner_cell = $('<div/>').addClass('inner_cell');
82 95 this.celltoolbar = new celltoolbar.CellToolbar({
83 96 cell: this,
84 97 notebook: this.notebook});
85 98 inner_cell.append(this.celltoolbar.element);
86 99 var input_area = $('<div/>').addClass('input_area');
87 100 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
88 101 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
89 102 // The tabindex=-1 makes this div focusable.
90 103 var render_area = $('<div/>').addClass('text_cell_render rendered_html')
91 104 .attr('tabindex','-1');
92 105 inner_cell.append(input_area).append(render_area);
93 106 cell.append(inner_cell);
94 107 this.element = cell;
95 108 };
96 109
97 110
98 111 // Cell level actions
99 112
100 113 TextCell.prototype.select = function () {
101 114 var cont = Cell.prototype.select.apply(this);
102 115 if (cont) {
103 116 if (this.mode === 'edit') {
104 117 this.code_mirror.refresh();
105 118 }
106 119 }
107 120 return cont;
108 121 };
109 122
110 123 TextCell.prototype.unrender = function () {
111 124 if (this.read_only) return;
112 125 var cont = Cell.prototype.unrender.apply(this);
113 126 if (cont) {
114 127 var text_cell = this.element;
115 128 var output = text_cell.find("div.text_cell_render");
116 129 if (this.get_text() === this.placeholder) {
117 130 this.set_text('');
118 131 }
119 132 this.refresh();
120 133 }
121 134 return cont;
122 135 };
123 136
124 137 TextCell.prototype.execute = function () {
125 138 this.render();
126 139 };
127 140
128 141 /**
129 142 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
130 143 * @method get_text
131 144 * @retrun {string} CodeMirror current text value
132 145 */
133 146 TextCell.prototype.get_text = function() {
134 147 return this.code_mirror.getValue();
135 148 };
136 149
137 150 /**
138 151 * @param {string} text - Codemiror text value
139 152 * @see TextCell#get_text
140 153 * @method set_text
141 154 * */
142 155 TextCell.prototype.set_text = function(text) {
143 156 this.code_mirror.setValue(text);
144 157 this.unrender();
145 158 this.code_mirror.refresh();
146 159 };
147 160
148 161 /**
149 162 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
150 163 * @method get_rendered
151 164 * */
152 165 TextCell.prototype.get_rendered = function() {
153 166 return this.element.find('div.text_cell_render').html();
154 167 };
155 168
156 169 /**
157 170 * @method set_rendered
158 171 */
159 172 TextCell.prototype.set_rendered = function(text) {
160 173 this.element.find('div.text_cell_render').html(text);
161 174 };
162 175
163 176
164 177 /**
165 178 * Create Text cell from JSON
166 179 * @param {json} data - JSON serialized text-cell
167 180 * @method fromJSON
168 181 */
169 182 TextCell.prototype.fromJSON = function (data) {
170 183 Cell.prototype.fromJSON.apply(this, arguments);
171 184 if (data.cell_type === this.cell_type) {
172 185 if (data.source !== undefined) {
173 186 this.set_text(data.source);
174 187 // make this value the starting point, so that we can only undo
175 188 // to this state, instead of a blank cell
176 189 this.code_mirror.clearHistory();
177 190 // TODO: This HTML needs to be treated as potentially dangerous
178 191 // user input and should be handled before set_rendered.
179 192 this.set_rendered(data.rendered || '');
180 193 this.rendered = false;
181 194 this.render();
182 195 }
183 196 }
184 197 };
185 198
186 199 /** Generate JSON from cell
187 200 * @return {object} cell data serialised to json
188 201 */
189 202 TextCell.prototype.toJSON = function () {
190 203 var data = Cell.prototype.toJSON.apply(this);
191 204 data.source = this.get_text();
192 205 if (data.source == this.placeholder) {
193 206 data.source = "";
194 207 }
195 208 return data;
196 209 };
197 210
198 211
199 212 var MarkdownCell = function (options) {
200 213 /**
201 214 * Constructor
202 215 *
203 216 * Parameters:
204 217 * options: dictionary
205 218 * Dictionary of keyword arguments.
206 219 * events: $(Events) instance
207 * config: dictionary
220 * config: ConfigSection instance
208 221 * keyboard_manager: KeyboardManager instance
209 222 * notebook: Notebook instance
210 223 */
211 224 options = options || {};
212 var config = utils.mergeopt(MarkdownCell, options.config);
225 var config = utils.mergeopt(MarkdownCell, {});
213 226 TextCell.apply(this, [$.extend({}, options, {config: config})]);
214 227
228 this.class_config = new configmod.ConfigWithDefaults(options.config,
229 {}, 'MarkdownCell');
215 230 this.cell_type = 'markdown';
216 231 };
217 232
218 233 MarkdownCell.options_default = {
219 234 cm_config: {
220 235 mode: 'ipythongfm'
221 236 },
222 237 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
223 238 };
224 239
225 240 MarkdownCell.prototype = Object.create(TextCell.prototype);
226 241
227 242 MarkdownCell.prototype.set_heading_level = function (level) {
228 243 /**
229 244 * make a markdown cell a heading
230 245 */
231 246 level = level || 1;
232 247 var source = this.get_text();
233 248 source = source.replace(/^(#*)\s?/,
234 249 new Array(level + 1).join('#') + ' ');
235 250 this.set_text(source);
236 251 this.refresh();
237 252 if (this.rendered) {
238 253 this.render();
239 254 }
240 255 };
241 256
242 257 /**
243 258 * @method render
244 259 */
245 260 MarkdownCell.prototype.render = function () {
246 261 var cont = TextCell.prototype.render.apply(this);
247 262 if (cont) {
248 263 var that = this;
249 264 var text = this.get_text();
250 265 var math = null;
251 266 if (text === "") { text = this.placeholder; }
252 267 var text_and_math = mathjaxutils.remove_math(text);
253 268 text = text_and_math[0];
254 269 math = text_and_math[1];
255 270 marked(text, function (err, html) {
256 271 html = mathjaxutils.replace_math(html, math);
257 272 html = security.sanitize_html(html);
258 273 html = $($.parseHTML(html));
259 274 // add anchors to headings
260 275 html.find(":header").addBack(":header").each(function (i, h) {
261 276 h = $(h);
262 277 var hash = h.text().replace(/ /g, '-');
263 278 h.attr('id', hash);
264 279 h.append(
265 280 $('<a/>')
266 281 .addClass('anchor-link')
267 282 .attr('href', '#' + hash)
268 283 .text('¶')
269 284 );
270 285 });
271 286 // links in markdown cells should open in new tabs
272 287 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
273 288 that.set_rendered(html);
274 289 that.typeset();
275 290 that.events.trigger("rendered.MarkdownCell", {cell: that});
276 291 });
277 292 }
278 293 return cont;
279 294 };
280 295
281 296
282 297 var RawCell = function (options) {
283 298 /**
284 299 * Constructor
285 300 *
286 301 * Parameters:
287 302 * options: dictionary
288 303 * Dictionary of keyword arguments.
289 304 * events: $(Events) instance
290 * config: dictionary
305 * config: ConfigSection instance
291 306 * keyboard_manager: KeyboardManager instance
292 307 * notebook: Notebook instance
293 308 */
294 309 options = options || {};
295 var config = utils.mergeopt(RawCell, options.config);
310 var config = utils.mergeopt(RawCell, {});
296 311 TextCell.apply(this, [$.extend({}, options, {config: config})]);
297 312
313 this.class_config = new configmod.ConfigWithDefaults(options.config,
314 RawCell.config_defaults, 'RawCell');
298 315 this.cell_type = 'raw';
299 316 };
300 317
301 318 RawCell.options_default = {
302 319 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
303 320 "It will not be rendered in the notebook. " +
304 321 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
305 322 };
323
324 RawCell.config_defaults = {
325 highlight_modes : {
326 'diff' :{'reg':[/^diff/]}
327 },
328 };
306 329
307 330 RawCell.prototype = Object.create(TextCell.prototype);
308 331
309 332 /** @method bind_events **/
310 333 RawCell.prototype.bind_events = function () {
311 334 TextCell.prototype.bind_events.apply(this);
312 335 var that = this;
313 336 this.element.focusout(function() {
314 337 that.auto_highlight();
315 338 that.render();
316 339 });
317 340
318 341 this.code_mirror.on('focus', function() { that.unrender(); });
319 342 };
320 343
321 344 /**
322 345 * Trigger autodetection of highlight scheme for current cell
323 346 * @method auto_highlight
324 347 */
325 348 RawCell.prototype.auto_highlight = function () {
326 this._auto_highlight(this.config.raw_cell_highlight);
349 this._auto_highlight(this.class_config.get_sync('highlight_modes'));
327 350 };
328 351
329 352 /** @method render **/
330 353 RawCell.prototype.render = function () {
331 354 var cont = TextCell.prototype.render.apply(this);
332 355 if (cont){
333 356 var text = this.get_text();
334 357 if (text === "") { text = this.placeholder; }
335 358 this.set_text(text);
336 359 this.element.removeClass('rendered');
337 360 }
338 361 return cont;
339 362 };
340 363
341 364 // Backwards compatability.
342 365 IPython.TextCell = TextCell;
343 366 IPython.MarkdownCell = MarkdownCell;
344 367 IPython.RawCell = RawCell;
345 368
346 369 var textcell = {
347 370 TextCell: TextCell,
348 371 MarkdownCell: MarkdownCell,
349 372 RawCell: RawCell
350 373 };
351 374 return textcell;
352 375 });
@@ -1,68 +1,129 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'jquery',
6 6 'base/js/utils',
7 7 ],
8 8 function($, utils) {
9 9 "use strict";
10 10 var ConfigSection = function(section_name, options) {
11 11 this.section_name = section_name;
12 12 this.base_url = options.base_url;
13 13 this.data = {};
14 14
15 15 var that = this;
16 16
17 17 /* .loaded is a promise, fulfilled the first time the config is loaded
18 18 * from the server. Code can do:
19 19 * conf.loaded.then(function() { ... using conf.data ... });
20 20 */
21 21 this._one_load_finished = false;
22 22 this.loaded = new Promise(function(resolve, reject) {
23 23 that._finish_firstload = resolve;
24 24 });
25 25 };
26 26
27 27 ConfigSection.prototype.api_url = function() {
28 28 return utils.url_join_encode(this.base_url, 'api/config', this.section_name);
29 29 };
30 30
31 31 ConfigSection.prototype._load_done = function() {
32 32 if (!this._one_load_finished) {
33 33 this._one_load_finished = true;
34 34 this._finish_firstload();
35 35 }
36 36 };
37 37
38 38 ConfigSection.prototype.load = function() {
39 39 var that = this;
40 40 return utils.promising_ajax(this.api_url(), {
41 41 cache: false,
42 42 type: "GET",
43 43 dataType: "json",
44 44 }).then(function(data) {
45 45 that.data = data;
46 46 that._load_done();
47 47 return data;
48 48 });
49 49 };
50 50
51 /**
52 * Modify the config values stored. Update the local data immediately,
53 * send the change to the server, and use the updated data from the server
54 * when the reply comes.
55 */
51 56 ConfigSection.prototype.update = function(newdata) {
57 $.extend(true, this.data, newdata); // true -> recursive update
58
52 59 var that = this;
53 60 return utils.promising_ajax(this.api_url(), {
54 61 processData: false,
55 62 type : "PATCH",
56 63 data: JSON.stringify(newdata),
57 64 dataType : "json",
58 65 contentType: 'application/json',
59 66 }).then(function(data) {
60 67 that.data = data;
61 68 that._load_done();
62 69 return data;
63 70 });
64 71 };
65 72
66 return {ConfigSection: ConfigSection};
73
74 var ConfigWithDefaults = function(section, defaults, classname) {
75 this.section = section;
76 this.defaults = defaults;
77 this.classname = classname;
78 };
79
80 ConfigWithDefaults.prototype._class_data = function() {
81 if (this.classname) {
82 return this.section.data[this.classname] || {};
83 } else {
84 return this.section.data
85 }
86 };
87
88 /**
89 * Wait for config to have loaded, then get a value or the default.
90 * Returns a promise.
91 */
92 ConfigWithDefaults.prototype.get = function(key) {
93 var that = this;
94 return this.section.loaded.then(function() {
95 return this._class_data()[key] || this.defaults[key]
96 });
97 };
98
99 /**
100 * Return a config value. If config is not yet loaded, return the default
101 * instead of waiting for it to load.
102 */
103 ConfigWithDefaults.prototype.get_sync = function(key) {
104 return this._class_data()[key] || this.defaults[key];
105 };
106
107 /**
108 * Set a config value. Send the update to the server, and change our
109 * local copy of the data immediately.
110 * Returns a promise which is fulfilled when the server replies to the
111 * change.
112 */
113 ConfigWithDefaults.prototype.set = function(key, value) {
114 var d = {};
115 d[key] = value;
116 if (this.classname) {
117 var d2 = {};
118 d2[this.classname] = d;
119 return this.section.update(d2);
120 } else {
121 return this.section.update(d);
122 }
123 };
124
125 return {ConfigSection: ConfigSection,
126 ConfigWithDefaults: ConfigWithDefaults,
127 };
67 128
68 129 });
@@ -1,77 +1,77 b''
1 1
2 2 // Test
3 3 casper.notebook_test(function () {
4 4 var a = 'print("a")';
5 5 var index = this.append_cell(a);
6 6 this.execute_cell_then(index);
7 7
8 8 var b = 'print("b")';
9 9 index = this.append_cell(b);
10 10 this.execute_cell_then(index);
11 11
12 12 var c = 'print("c")';
13 13 index = this.append_cell(c);
14 14 this.execute_cell_then(index);
15 15
16 16 this.thenEvaluate(function() {
17 17 IPython.notebook.default_cell_type = 'code';
18 18 });
19 19
20 20 this.then(function () {
21 21 // Cell insertion
22 22 this.select_cell(2);
23 23 this.trigger_keydown('m'); // Make it markdown
24 24 this.trigger_keydown('a'); // Creates one cell
25 25 this.test.assertEquals(this.get_cell_text(2), '', 'a; New cell 2 text is empty');
26 26 this.test.assertEquals(this.get_cell(2).cell_type, 'code', 'a; inserts a code cell');
27 27 this.validate_notebook_state('a', 'command', 2);
28 28 this.trigger_keydown('b'); // Creates one cell
29 29 this.test.assertEquals(this.get_cell_text(2), '', 'b; Cell 2 text is still empty');
30 30 this.test.assertEquals(this.get_cell_text(3), '', 'b; New cell 3 text is empty');
31 31 this.test.assertEquals(this.get_cell(3).cell_type, 'code', 'b; inserts a code cell');
32 32 this.validate_notebook_state('b', 'command', 3);
33 33 });
34 34
35 35 this.thenEvaluate(function() {
36 IPython.notebook.default_cell_type = 'selected';
36 IPython.notebook.class_config.set('default_cell_type', 'selected');
37 37 });
38 38
39 39 this.then(function () {
40 40 this.select_cell(2);
41 41 this.trigger_keydown('m'); // switch it to markdown for the next test
42 42 this.test.assertEquals(this.get_cell(2).cell_type, 'markdown', 'test cell is markdown');
43 43 this.trigger_keydown('a'); // new cell above
44 44 this.test.assertEquals(this.get_cell(2).cell_type, 'markdown', 'a; inserts a markdown cell when markdown selected');
45 45 this.trigger_keydown('b'); // new cell below
46 46 this.test.assertEquals(this.get_cell(3).cell_type, 'markdown', 'b; inserts a markdown cell when markdown selected');
47 47 });
48 48
49 49 this.thenEvaluate(function() {
50 IPython.notebook.default_cell_type = 'above';
50 IPython.notebook.class_config.set('default_cell_type', 'above');
51 51 });
52 52
53 53 this.then(function () {
54 54 this.select_cell(2);
55 55 this.trigger_keydown('y'); // switch it to code for the next test
56 56 this.test.assertEquals(this.get_cell(2).cell_type, 'code', 'test cell is code');
57 57 this.trigger_keydown('b'); // new cell below
58 58 this.test.assertEquals(this.get_cell(3).cell_type, 'code', 'b; inserts a code cell below code cell');
59 59 this.trigger_keydown('a'); // new cell above
60 60 this.test.assertEquals(this.get_cell(3).cell_type, 'code', 'a; inserts a code cell above code cell');
61 61 });
62 62
63 63 this.thenEvaluate(function() {
64 IPython.notebook.default_cell_type = 'below';
64 IPython.notebook.class_config.set('default_cell_type', 'below');
65 65 });
66 66
67 67 this.then(function () {
68 68 this.select_cell(2);
69 69 this.trigger_keydown('r'); // switch it to markdown for the next test
70 70 this.test.assertEquals(this.get_cell(2).cell_type, 'raw', 'test cell is raw');
71 71 this.trigger_keydown('a'); // new cell above
72 72 this.test.assertEquals(this.get_cell(2).cell_type, 'raw', 'a; inserts a raw cell above raw cell');
73 73 this.trigger_keydown('y'); // switch it to code for the next test
74 74 this.trigger_keydown('b'); // new cell below
75 75 this.test.assertEquals(this.get_cell(3).cell_type, 'raw', 'b; inserts a raw cell below raw cell');
76 76 });
77 77 });
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now