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