##// END OF EJS Templates
Merge pull request #7522 from minrk/store-scroll...
Matthias Bussonnier -
r20141:d0fbe137 merge
parent child Browse files
Show More
@@ -1,653 +1,651 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3 /**
4 4 *
5 5 *
6 6 * @module codecell
7 7 * @namespace codecell
8 8 * @class CodeCell
9 9 */
10 10
11 11
12 12 define([
13 13 'base/js/namespace',
14 14 'jquery',
15 15 'base/js/utils',
16 16 'base/js/keyboard',
17 17 'services/config',
18 18 'notebook/js/cell',
19 19 'notebook/js/outputarea',
20 20 'notebook/js/completer',
21 21 'notebook/js/celltoolbar',
22 22 'codemirror/lib/codemirror',
23 23 'codemirror/mode/python/python',
24 24 'notebook/js/codemirror-ipython'
25 25 ], function(IPython,
26 26 $,
27 27 utils,
28 28 keyboard,
29 29 configmod,
30 30 cell,
31 31 outputarea,
32 32 completer,
33 33 celltoolbar,
34 34 CodeMirror,
35 35 cmpython,
36 36 cmip
37 37 ) {
38 38 "use strict";
39 39
40 40 var Cell = cell.Cell;
41 41
42 42 /* local util for codemirror */
43 43 var posEq = function(a, b) {return a.line === b.line && a.ch === b.ch;};
44 44
45 45 /**
46 46 *
47 47 * function to delete until previous non blanking space character
48 48 * or first multiple of 4 tabstop.
49 49 * @private
50 50 */
51 51 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
52 52 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
53 53 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
54 54 var cur = cm.getCursor(), line = cm.getLine(cur.line);
55 55 var tabsize = cm.getOption('tabSize');
56 56 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
57 57 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
58 58 var select = cm.getRange(from,cur);
59 59 if( select.match(/^\ +$/) !== null){
60 60 cm.replaceRange("",from,cur);
61 61 } else {
62 62 cm.deleteH(-1,"char");
63 63 }
64 64 };
65 65
66 66 var keycodes = keyboard.keycodes;
67 67
68 68 var CodeCell = function (kernel, options) {
69 69 /**
70 70 * Constructor
71 71 *
72 72 * A Cell conceived to write code.
73 73 *
74 74 * Parameters:
75 75 * kernel: Kernel instance
76 76 * The kernel doesn't have to be set at creation time, in that case
77 77 * it will be null and set_kernel has to be called later.
78 78 * options: dictionary
79 79 * Dictionary of keyword arguments.
80 80 * events: $(Events) instance
81 81 * config: dictionary
82 82 * keyboard_manager: KeyboardManager instance
83 83 * notebook: Notebook instance
84 84 * tooltip: Tooltip instance
85 85 */
86 86 this.kernel = kernel || null;
87 87 this.notebook = options.notebook;
88 88 this.collapsed = false;
89 89 this.events = options.events;
90 90 this.tooltip = options.tooltip;
91 91 this.config = options.config;
92 92 this.class_config = new configmod.ConfigWithDefaults(this.config,
93 93 CodeCell.config_defaults, 'CodeCell');
94 94
95 95 // create all attributed in constructor function
96 96 // even if null for V8 VM optimisation
97 97 this.input_prompt_number = null;
98 98 this.celltoolbar = null;
99 99 this.output_area = null;
100 100
101 101 this.last_msg_id = null;
102 102 this.completer = null;
103 103 this.widget_views = [];
104 104 this._widgets_live = true;
105 105
106 106 Cell.apply(this,[{
107 107 config: $.extend({}, CodeCell.options_default),
108 108 keyboard_manager: options.keyboard_manager,
109 109 events: this.events}]);
110 110
111 111 // Attributes we want to override in this subclass.
112 112 this.cell_type = "code";
113 113 var that = this;
114 114 this.element.focusout(
115 115 function() { that.auto_highlight(); }
116 116 );
117 117 };
118 118
119 119 CodeCell.options_default = {
120 120 cm_config : {
121 121 extraKeys: {
122 122 "Tab" : "indentMore",
123 123 "Shift-Tab" : "indentLess",
124 124 "Backspace" : "delSpaceToPrevTabStop",
125 125 "Cmd-/" : "toggleComment",
126 126 "Ctrl-/" : "toggleComment"
127 127 },
128 128 mode: 'ipython',
129 129 theme: 'ipython',
130 130 matchBrackets: true
131 131 }
132 132 };
133 133
134 134 CodeCell.config_defaults = {
135 135 highlight_modes : {
136 136 'magic_javascript' :{'reg':[/^%%javascript/]},
137 137 'magic_perl' :{'reg':[/^%%perl/]},
138 138 'magic_ruby' :{'reg':[/^%%ruby/]},
139 139 'magic_python' :{'reg':[/^%%python3?/]},
140 140 'magic_shell' :{'reg':[/^%%bash/]},
141 141 'magic_r' :{'reg':[/^%%R/]},
142 142 'magic_text/x-cython' :{'reg':[/^%%cython/]},
143 143 },
144 144 };
145 145
146 146 CodeCell.msg_cells = {};
147 147
148 148 CodeCell.prototype = Object.create(Cell.prototype);
149 149
150 150 /** @method create_element */
151 151 CodeCell.prototype.create_element = function () {
152 152 Cell.prototype.create_element.apply(this, arguments);
153 153 var that = this;
154 154
155 155 var cell = $('<div></div>').addClass('cell code_cell');
156 156 cell.attr('tabindex','2');
157 157
158 158 var input = $('<div></div>').addClass('input');
159 159 var prompt = $('<div/>').addClass('prompt input_prompt');
160 160 var inner_cell = $('<div/>').addClass('inner_cell');
161 161 this.celltoolbar = new celltoolbar.CellToolbar({
162 162 cell: this,
163 163 notebook: this.notebook});
164 164 inner_cell.append(this.celltoolbar.element);
165 165 var input_area = $('<div/>').addClass('input_area');
166 166 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
167 167 // In case of bugs that put the keyboard manager into an inconsistent state,
168 168 // ensure KM is enabled when CodeMirror is focused:
169 169 this.code_mirror.on('focus', function () {
170 170 if (that.keyboard_manager) {
171 171 that.keyboard_manager.enable();
172 172 }
173 173 });
174 174 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this));
175 175 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
176 176 inner_cell.append(input_area);
177 177 input.append(prompt).append(inner_cell);
178 178
179 179 var widget_area = $('<div/>')
180 180 .addClass('widget-area')
181 181 .hide();
182 182 this.widget_area = widget_area;
183 183 var widget_prompt = $('<div/>')
184 184 .addClass('prompt')
185 185 .appendTo(widget_area);
186 186 var widget_subarea = $('<div/>')
187 187 .addClass('widget-subarea')
188 188 .appendTo(widget_area);
189 189 this.widget_subarea = widget_subarea;
190 190 var that = this;
191 191 var widget_clear_buton = $('<button />')
192 192 .addClass('close')
193 193 .html('&times;')
194 194 .click(function() {
195 195 widget_area.slideUp('', function(){
196 196 for (var i = 0; i < that.widget_views.length; i++) {
197 197 var view = that.widget_views[i];
198 198 view.remove();
199 199
200 200 // Remove widget live events.
201 201 view.off('comm:live', that._widget_live);
202 202 view.off('comm:dead', that._widget_dead);
203 203 }
204 204 that.widget_views = [];
205 205 widget_subarea.html('');
206 206 });
207 207 })
208 208 .appendTo(widget_prompt);
209 209
210 210 var output = $('<div></div>');
211 211 cell.append(input).append(widget_area).append(output);
212 212 this.element = cell;
213 213 this.output_area = new outputarea.OutputArea({
214 214 selector: output,
215 215 prompt_area: true,
216 216 events: this.events,
217 217 keyboard_manager: this.keyboard_manager});
218 218 this.completer = new completer.Completer(this, this.events);
219 219 };
220 220
221 221 /**
222 222 * Display a widget view in the cell.
223 223 */
224 224 CodeCell.prototype.display_widget_view = function(view_promise) {
225 225
226 226 // Display a dummy element
227 227 var dummy = $('<div/>');
228 228 this.widget_subarea.append(dummy);
229 229
230 230 // Display the view.
231 231 var that = this;
232 232 return view_promise.then(function(view) {
233 233 that.widget_area.show();
234 234 dummy.replaceWith(view.$el);
235 235 that.widget_views.push(view);
236 236
237 237 // Check the live state of the view's model.
238 238 if (view.model.comm_live) {
239 239 that._widget_live(view);
240 240 } else {
241 241 that._widget_dead(view);
242 242 }
243 243
244 244 // Listen to comm live events for the view.
245 245 view.on('comm:live', that._widget_live, that);
246 246 view.on('comm:dead', that._widget_dead, that);
247 247 return view;
248 248 });
249 249 };
250 250
251 251 /**
252 252 * Handles when a widget loses it's comm connection.
253 253 * @param {WidgetView} view
254 254 */
255 255 CodeCell.prototype._widget_dead = function(view) {
256 256 if (this._widgets_live) {
257 257 this._widgets_live = false;
258 258 this.widget_area.addClass('connection-problems');
259 259 }
260 260
261 261 };
262 262
263 263 /**
264 264 * Handles when a widget is connected to a live comm.
265 265 * @param {WidgetView} view
266 266 */
267 267 CodeCell.prototype._widget_live = function(view) {
268 268 if (!this._widgets_live) {
269 269 // Check that the other widgets are live too. O(N) operation.
270 270 // Abort the function at the first dead widget found.
271 271 for (var i = 0; i < this.widget_views.length; i++) {
272 272 if (!this.widget_views[i].model.comm_live) return;
273 273 }
274 274 this._widgets_live = true;
275 275 this.widget_area.removeClass('connection-problems');
276 276 }
277 277 };
278 278
279 279 /** @method bind_events */
280 280 CodeCell.prototype.bind_events = function () {
281 281 Cell.prototype.bind_events.apply(this);
282 282 var that = this;
283 283
284 284 this.element.focusout(
285 285 function() { that.auto_highlight(); }
286 286 );
287 287 };
288 288
289 289
290 290 /**
291 291 * This method gets called in CodeMirror's onKeyDown/onKeyPress
292 292 * handlers and is used to provide custom key handling. Its return
293 293 * value is used to determine if CodeMirror should ignore the event:
294 294 * true = ignore, false = don't ignore.
295 295 * @method handle_codemirror_keyevent
296 296 */
297 297
298 298 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
299 299
300 300 var that = this;
301 301 // whatever key is pressed, first, cancel the tooltip request before
302 302 // they are sent, and remove tooltip if any, except for tab again
303 303 var tooltip_closed = null;
304 304 if (event.type === 'keydown' && event.which !== keycodes.tab ) {
305 305 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
306 306 }
307 307
308 308 var cur = editor.getCursor();
309 309 if (event.keyCode === keycodes.enter){
310 310 this.auto_highlight();
311 311 }
312 312
313 313 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
314 314 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
315 315 // browser and keyboard layout !
316 316 // Pressing '(' , request tooltip, don't forget to reappend it
317 317 // The second argument says to hide the tooltip if the docstring
318 318 // is actually empty
319 319 this.tooltip.pending(that, true);
320 320 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
321 321 // If tooltip is active, cancel it. The call to
322 322 // remove_and_cancel_tooltip above doesn't pass, force=true.
323 323 // Because of this it won't actually close the tooltip
324 324 // if it is in sticky mode. Thus, we have to check again if it is open
325 325 // and close it with force=true.
326 326 if (!this.tooltip._hidden) {
327 327 this.tooltip.remove_and_cancel_tooltip(true);
328 328 }
329 329 // If we closed the tooltip, don't let CM or the global handlers
330 330 // handle this event.
331 331 event.codemirrorIgnore = true;
332 332 event.preventDefault();
333 333 return true;
334 334 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
335 335 if (editor.somethingSelected() || editor.getSelections().length !== 1){
336 336 var anchor = editor.getCursor("anchor");
337 337 var head = editor.getCursor("head");
338 338 if( anchor.line !== head.line){
339 339 return false;
340 340 }
341 341 }
342 342 this.tooltip.request(that);
343 343 event.codemirrorIgnore = true;
344 344 event.preventDefault();
345 345 return true;
346 346 } else if (event.keyCode === keycodes.tab && event.type === 'keydown') {
347 347 // Tab completion.
348 348 this.tooltip.remove_and_cancel_tooltip();
349 349
350 350 // completion does not work on multicursor, it might be possible though in some cases
351 351 if (editor.somethingSelected() || editor.getSelections().length > 1) {
352 352 return false;
353 353 }
354 354 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
355 355 if (pre_cursor.trim() === "") {
356 356 // Don't autocomplete if the part of the line before the cursor
357 357 // is empty. In this case, let CodeMirror handle indentation.
358 358 return false;
359 359 } else {
360 360 event.codemirrorIgnore = true;
361 361 event.preventDefault();
362 362 this.completer.startCompletion();
363 363 return true;
364 364 }
365 365 }
366 366
367 367 // keyboard event wasn't one of those unique to code cells, let's see
368 368 // if it's one of the generic ones (i.e. check edit mode shortcuts)
369 369 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
370 370 };
371 371
372 372 // Kernel related calls.
373 373
374 374 CodeCell.prototype.set_kernel = function (kernel) {
375 375 this.kernel = kernel;
376 376 };
377 377
378 378 /**
379 379 * Execute current code cell to the kernel
380 380 * @method execute
381 381 */
382 382 CodeCell.prototype.execute = function (stop_on_error) {
383 383 if (!this.kernel || !this.kernel.is_connected()) {
384 384 console.log("Can't execute, kernel is not connected.");
385 385 return;
386 386 }
387 387
388 388 this.output_area.clear_output(false, true);
389 389
390 390 if (stop_on_error === undefined) {
391 391 stop_on_error = true;
392 392 }
393 393
394 394 // Clear widget area
395 395 for (var i = 0; i < this.widget_views.length; i++) {
396 396 var view = this.widget_views[i];
397 397 view.remove();
398 398
399 399 // Remove widget live events.
400 400 view.off('comm:live', this._widget_live);
401 401 view.off('comm:dead', this._widget_dead);
402 402 }
403 403 this.widget_views = [];
404 404 this.widget_subarea.html('');
405 405 this.widget_subarea.height('');
406 406 this.widget_area.height('');
407 407 this.widget_area.hide();
408 408
409 409 this.set_input_prompt('*');
410 410 this.element.addClass("running");
411 411 if (this.last_msg_id) {
412 412 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
413 413 }
414 414 var callbacks = this.get_callbacks();
415 415
416 416 var old_msg_id = this.last_msg_id;
417 417 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true,
418 418 stop_on_error : stop_on_error});
419 419 if (old_msg_id) {
420 420 delete CodeCell.msg_cells[old_msg_id];
421 421 }
422 422 CodeCell.msg_cells[this.last_msg_id] = this;
423 423 this.render();
424 424 this.events.trigger('execute.CodeCell', {cell: this});
425 425 };
426 426
427 427 /**
428 428 * Construct the default callbacks for
429 429 * @method get_callbacks
430 430 */
431 431 CodeCell.prototype.get_callbacks = function () {
432 432 var that = this;
433 433 return {
434 434 shell : {
435 435 reply : $.proxy(this._handle_execute_reply, this),
436 436 payload : {
437 437 set_next_input : $.proxy(this._handle_set_next_input, this),
438 438 page : $.proxy(this._open_with_pager, this)
439 439 }
440 440 },
441 441 iopub : {
442 442 output : function() {
443 443 that.output_area.handle_output.apply(that.output_area, arguments);
444 444 },
445 445 clear_output : function() {
446 446 that.output_area.handle_clear_output.apply(that.output_area, arguments);
447 447 },
448 448 },
449 449 input : $.proxy(this._handle_input_request, this)
450 450 };
451 451 };
452 452
453 453 CodeCell.prototype._open_with_pager = function (payload) {
454 454 this.events.trigger('open_with_text.Pager', payload);
455 455 };
456 456
457 457 /**
458 458 * @method _handle_execute_reply
459 459 * @private
460 460 */
461 461 CodeCell.prototype._handle_execute_reply = function (msg) {
462 462 this.set_input_prompt(msg.content.execution_count);
463 463 this.element.removeClass("running");
464 464 this.events.trigger('set_dirty.Notebook', {value: true});
465 465 };
466 466
467 467 /**
468 468 * @method _handle_set_next_input
469 469 * @private
470 470 */
471 471 CodeCell.prototype._handle_set_next_input = function (payload) {
472 472 var data = {'cell': this, 'text': payload.text, replace: payload.replace};
473 473 this.events.trigger('set_next_input.Notebook', data);
474 474 };
475 475
476 476 /**
477 477 * @method _handle_input_request
478 478 * @private
479 479 */
480 480 CodeCell.prototype._handle_input_request = function (msg) {
481 481 this.output_area.append_raw_input(msg);
482 482 };
483 483
484 484
485 485 // Basic cell manipulation.
486 486
487 487 CodeCell.prototype.select = function () {
488 488 var cont = Cell.prototype.select.apply(this);
489 489 if (cont) {
490 490 this.code_mirror.refresh();
491 491 this.auto_highlight();
492 492 }
493 493 return cont;
494 494 };
495 495
496 496 CodeCell.prototype.render = function () {
497 497 var cont = Cell.prototype.render.apply(this);
498 498 // Always execute, even if we are already in the rendered state
499 499 return cont;
500 500 };
501 501
502 502 CodeCell.prototype.select_all = function () {
503 503 var start = {line: 0, ch: 0};
504 504 var nlines = this.code_mirror.lineCount();
505 505 var last_line = this.code_mirror.getLine(nlines-1);
506 506 var end = {line: nlines-1, ch: last_line.length};
507 507 this.code_mirror.setSelection(start, end);
508 508 };
509 509
510 510
511 511 CodeCell.prototype.collapse_output = function () {
512 512 this.output_area.collapse();
513 513 };
514 514
515 515
516 516 CodeCell.prototype.expand_output = function () {
517 517 this.output_area.expand();
518 518 this.output_area.unscroll_area();
519 519 };
520 520
521 521 CodeCell.prototype.scroll_output = function () {
522 522 this.output_area.expand();
523 523 this.output_area.scroll_if_long();
524 524 };
525 525
526 526 CodeCell.prototype.toggle_output = function () {
527 527 this.output_area.toggle_output();
528 528 };
529 529
530 530 CodeCell.prototype.toggle_output_scroll = function () {
531 531 this.output_area.toggle_scroll();
532 532 };
533 533
534 534
535 535 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
536 536 var ns;
537 537 if (prompt_value === undefined || prompt_value === null) {
538 538 ns = "&nbsp;";
539 539 } else {
540 540 ns = encodeURIComponent(prompt_value);
541 541 }
542 542 return 'In&nbsp;[' + ns + ']:';
543 543 };
544 544
545 545 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
546 546 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
547 547 for(var i=1; i < lines_number; i++) {
548 548 html.push(['...:']);
549 549 }
550 550 return html.join('<br/>');
551 551 };
552 552
553 553 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
554 554
555 555
556 556 CodeCell.prototype.set_input_prompt = function (number) {
557 557 var nline = 1;
558 558 if (this.code_mirror !== undefined) {
559 559 nline = this.code_mirror.lineCount();
560 560 }
561 561 this.input_prompt_number = number;
562 562 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
563 563 // This HTML call is okay because the user contents are escaped.
564 564 this.element.find('div.input_prompt').html(prompt_html);
565 565 };
566 566
567 567
568 568 CodeCell.prototype.clear_input = function () {
569 569 this.code_mirror.setValue('');
570 570 };
571 571
572 572
573 573 CodeCell.prototype.get_text = function () {
574 574 return this.code_mirror.getValue();
575 575 };
576 576
577 577
578 578 CodeCell.prototype.set_text = function (code) {
579 579 return this.code_mirror.setValue(code);
580 580 };
581 581
582 582
583 583 CodeCell.prototype.clear_output = function (wait) {
584 584 this.output_area.clear_output(wait);
585 585 this.set_input_prompt();
586 586 };
587 587
588 588
589 589 // JSON serialization
590 590
591 591 CodeCell.prototype.fromJSON = function (data) {
592 592 Cell.prototype.fromJSON.apply(this, arguments);
593 593 if (data.cell_type === 'code') {
594 594 if (data.source !== undefined) {
595 595 this.set_text(data.source);
596 596 // make this value the starting point, so that we can only undo
597 597 // to this state, instead of a blank cell
598 598 this.code_mirror.clearHistory();
599 599 this.auto_highlight();
600 600 }
601 601 this.set_input_prompt(data.execution_count);
602 602 this.output_area.trusted = data.metadata.trusted || false;
603 this.output_area.fromJSON(data.outputs);
604 if (data.metadata.collapsed !== undefined) {
605 if (data.metadata.collapsed) {
606 this.collapse_output();
607 } else {
608 this.expand_output();
609 }
610 }
603 this.output_area.fromJSON(data.outputs, data.metadata);
611 604 }
612 605 };
613 606
614 607
615 608 CodeCell.prototype.toJSON = function () {
616 609 var data = Cell.prototype.toJSON.apply(this);
617 610 data.source = this.get_text();
618 611 // is finite protect against undefined and '*' value
619 612 if (isFinite(this.input_prompt_number)) {
620 613 data.execution_count = this.input_prompt_number;
621 614 } else {
622 615 data.execution_count = null;
623 616 }
624 617 var outputs = this.output_area.toJSON();
625 618 data.outputs = outputs;
626 619 data.metadata.trusted = this.output_area.trusted;
627 620 data.metadata.collapsed = this.output_area.collapsed;
621 if (this.output_area.scroll_state === 'auto') {
622 delete data.metadata.scrolled;
623 } else {
624 data.metadata.scrolled = this.output_area.scroll_state;
625 }
628 626 return data;
629 627 };
630 628
631 629 /**
632 630 * handle cell level logic when a cell is unselected
633 631 * @method unselect
634 632 * @return is the action being taken
635 633 */
636 634 CodeCell.prototype.unselect = function () {
637 635 var cont = Cell.prototype.unselect.apply(this);
638 636 if (cont) {
639 637 // When a code cell is usnelected, make sure that the corresponding
640 638 // tooltip and completer to that cell is closed.
641 639 this.tooltip.remove_and_cancel_tooltip(true);
642 640 if (this.completer !== null) {
643 641 this.completer.close();
644 642 }
645 643 }
646 644 return cont;
647 645 };
648 646
649 647 // Backwards compatability.
650 648 IPython.CodeCell = CodeCell;
651 649
652 650 return {'CodeCell': CodeCell};
653 651 });
@@ -1,986 +1,989 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jqueryui',
7 7 'base/js/utils',
8 8 'base/js/security',
9 9 'base/js/keyboard',
10 10 'notebook/js/mathjaxutils',
11 11 'components/marked/lib/marked',
12 12 ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) {
13 13 "use strict";
14 14
15 15 /**
16 16 * @class OutputArea
17 17 *
18 18 * @constructor
19 19 */
20 20
21 21 var OutputArea = function (options) {
22 22 this.selector = options.selector;
23 23 this.events = options.events;
24 24 this.keyboard_manager = options.keyboard_manager;
25 25 this.wrapper = $(options.selector);
26 26 this.outputs = [];
27 27 this.collapsed = false;
28 28 this.scrolled = false;
29 this.scroll_state = 'auto';
29 30 this.trusted = true;
30 31 this.clear_queued = null;
31 32 if (options.prompt_area === undefined) {
32 33 this.prompt_area = true;
33 34 } else {
34 35 this.prompt_area = options.prompt_area;
35 36 }
36 37 this.create_elements();
37 38 this.style();
38 39 this.bind_events();
39 40 };
40 41
41 42
42 43 /**
43 44 * Class prototypes
44 45 **/
45 46
46 47 OutputArea.prototype.create_elements = function () {
47 48 this.element = $("<div/>");
48 49 this.collapse_button = $("<div/>");
49 50 this.prompt_overlay = $("<div/>");
50 51 this.wrapper.append(this.prompt_overlay);
51 52 this.wrapper.append(this.element);
52 53 this.wrapper.append(this.collapse_button);
53 54 };
54 55
55 56
56 57 OutputArea.prototype.style = function () {
57 58 this.collapse_button.hide();
58 59 this.prompt_overlay.hide();
59 60
60 61 this.wrapper.addClass('output_wrapper');
61 62 this.element.addClass('output');
62 63
63 64 this.collapse_button.addClass("btn btn-default output_collapsed");
64 65 this.collapse_button.attr('title', 'click to expand output');
65 66 this.collapse_button.text('. . .');
66 67
67 68 this.prompt_overlay.addClass('out_prompt_overlay prompt');
68 69 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
69 70
70 71 this.collapse();
71 72 };
72 73
73 74 /**
74 75 * Should the OutputArea scroll?
75 * Returns whether the height (in lines) exceeds a threshold.
76 *
77 * @private
78 * @method _should_scroll
79 * @param [lines=100]{Integer}
80 * @return {Bool}
76 * Returns whether the height (in lines) exceeds the current threshold.
77 * Threshold will be OutputArea.minimum_scroll_threshold if scroll_state=true (manually requested)
78 * or OutputArea.auto_scroll_threshold if scroll_state='auto'.
79 * This will always return false if scroll_state=false (scroll disabled).
81 80 *
82 81 */
83 OutputArea.prototype._should_scroll = function (lines) {
84 if (lines <=0 ){ return; }
85 if (!lines) {
86 lines = 100;
82 OutputArea.prototype._should_scroll = function () {
83 var threshold;
84 if (this.scroll_state === false) {
85 return false;
86 } else if (this.scroll_state === true) {
87 threshold = OutputArea.minimum_scroll_threshold;
88 } else {
89 threshold = OutputArea.auto_scroll_threshold;
90 }
91 if (threshold <=0) {
92 return false;
87 93 }
88 94 // line-height from http://stackoverflow.com/questions/1185151
89 95 var fontSize = this.element.css('font-size');
90 96 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
91
92 return (this.element.height() > lines * lineHeight);
97 return (this.element.height() > threshold * lineHeight);
93 98 };
94 99
95 100
96 101 OutputArea.prototype.bind_events = function () {
97 102 var that = this;
98 103 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
99 104 this.prompt_overlay.click(function () { that.toggle_scroll(); });
100 105
101 106 this.element.resize(function () {
102 107 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
103 108 if ( utils.browser[0] === "Firefox" ) {
104 109 return;
105 110 }
106 111 // maybe scroll output,
107 112 // if it's grown large enough and hasn't already been scrolled.
108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
113 if (!that.scrolled && that._should_scroll()) {
109 114 that.scroll_area();
110 115 }
111 116 });
112 117 this.collapse_button.click(function () {
113 118 that.expand();
114 119 });
115 120 };
116 121
117 122
118 123 OutputArea.prototype.collapse = function () {
119 124 if (!this.collapsed) {
120 125 this.element.hide();
121 126 this.prompt_overlay.hide();
122 127 if (this.element.html()){
123 128 this.collapse_button.show();
124 129 }
125 130 this.collapsed = true;
131 // collapsing output clears scroll state
132 this.scroll_state = 'auto';
126 133 }
127 134 };
128 135
129 136
130 137 OutputArea.prototype.expand = function () {
131 138 if (this.collapsed) {
132 139 this.collapse_button.hide();
133 140 this.element.show();
134 141 this.prompt_overlay.show();
135 142 this.collapsed = false;
143 this.scroll_if_long();
136 144 }
137 145 };
138 146
139 147
140 148 OutputArea.prototype.toggle_output = function () {
141 149 if (this.collapsed) {
142 150 this.expand();
143 151 } else {
144 152 this.collapse();
145 153 }
146 154 };
147 155
148 156
149 157 OutputArea.prototype.scroll_area = function () {
150 158 this.element.addClass('output_scroll');
151 159 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
152 160 this.scrolled = true;
153 161 };
154 162
155 163
156 164 OutputArea.prototype.unscroll_area = function () {
157 165 this.element.removeClass('output_scroll');
158 166 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
159 167 this.scrolled = false;
160 168 };
161 169
162 170 /**
171 * Scroll OutputArea if height exceeds a threshold.
163 172 *
164 * Scroll OutputArea if height supperior than a threshold (in lines).
165 *
166 * Threshold is a maximum number of lines. If unspecified, defaults to
167 * OutputArea.minimum_scroll_threshold.
168 *
169 * Negative threshold will prevent the OutputArea from ever scrolling.
170 *
171 * @method scroll_if_long
172 *
173 * @param [lines=20]{Number} Default to 20 if not set,
174 * behavior undefined for value of `0`.
173 * Threshold is OutputArea.minimum_scroll_threshold if scroll_state = true,
174 * OutputArea.auto_scroll_threshold if scroll_state='auto'.
175 175 *
176 176 **/
177 OutputArea.prototype.scroll_if_long = function (lines) {
178 var n = lines || OutputArea.minimum_scroll_threshold;
179 if(n <= 0){
180 return;
181 }
182
183 if (this._should_scroll(n)) {
177 OutputArea.prototype.scroll_if_long = function () {
178 var should_scroll = this._should_scroll();
179 if (!this.scrolled && should_scroll) {
184 180 // only allow scrolling long-enough output
185 181 this.scroll_area();
182 } else if (this.scrolled && !should_scroll) {
183 // scrolled and shouldn't be
184 this.unscroll_area();
186 185 }
187 186 };
188 187
189 188
190 189 OutputArea.prototype.toggle_scroll = function () {
190 if (this.scroll_state == 'auto') {
191 this.scroll_state = !this.scrolled;
192 } else {
193 this.scroll_state = !this.scroll_state;
194 }
191 195 if (this.scrolled) {
192 196 this.unscroll_area();
193 197 } else {
194 198 // only allow scrolling long-enough output
195 199 this.scroll_if_long();
196 200 }
197 201 };
198 202
199 203
200 204 // typeset with MathJax if MathJax is available
201 205 OutputArea.prototype.typeset = function () {
202 206 utils.typeset(this.element);
203 207 };
204 208
205 209
206 210 OutputArea.prototype.handle_output = function (msg) {
207 211 var json = {};
208 212 var msg_type = json.output_type = msg.header.msg_type;
209 213 var content = msg.content;
210 214 if (msg_type === "stream") {
211 215 json.text = content.text;
212 216 json.name = content.name;
213 217 } else if (msg_type === "display_data") {
214 218 json.data = content.data;
215 219 json.metadata = content.metadata;
216 220 } else if (msg_type === "execute_result") {
217 221 json.data = content.data;
218 222 json.metadata = content.metadata;
219 223 json.execution_count = content.execution_count;
220 224 } else if (msg_type === "error") {
221 225 json.ename = content.ename;
222 226 json.evalue = content.evalue;
223 227 json.traceback = content.traceback;
224 228 } else {
225 229 console.log("unhandled output message", msg);
226 230 return;
227 231 }
228 232 this.append_output(json);
229 233 };
230 234
231 235
232 236 OutputArea.output_types = [
233 237 'application/javascript',
234 238 'text/html',
235 239 'text/markdown',
236 240 'text/latex',
237 241 'image/svg+xml',
238 242 'image/png',
239 243 'image/jpeg',
240 244 'application/pdf',
241 245 'text/plain'
242 246 ];
243 247
244 248 OutputArea.prototype.validate_mimebundle = function (bundle) {
245 249 /** scrub invalid outputs */
246 250 if (typeof bundle.data !== 'object') {
247 251 console.warn("mimebundle missing data", bundle);
248 252 bundle.data = {};
249 253 }
250 254 if (typeof bundle.metadata !== 'object') {
251 255 console.warn("mimebundle missing metadata", bundle);
252 256 bundle.metadata = {};
253 257 }
254 258 var data = bundle.data;
255 259 $.map(OutputArea.output_types, function(key){
256 260 if (key !== 'application/json' &&
257 261 data[key] !== undefined &&
258 262 typeof data[key] !== 'string'
259 263 ) {
260 264 console.log("Invalid type for " + key, data[key]);
261 265 delete data[key];
262 266 }
263 267 });
264 268 return bundle;
265 269 };
266 270
267 271 OutputArea.prototype.append_output = function (json) {
268 272 this.expand();
269 273
270 274 // Clear the output if clear is queued.
271 275 var needs_height_reset = false;
272 276 if (this.clear_queued) {
273 277 this.clear_output(false);
274 278 needs_height_reset = true;
275 279 }
276 280
277 281 var record_output = true;
278 282 switch(json.output_type) {
279 283 case 'execute_result':
280 284 json = this.validate_mimebundle(json);
281 285 this.append_execute_result(json);
282 286 break;
283 287 case 'stream':
284 288 // append_stream might have merged the output with earlier stream output
285 289 record_output = this.append_stream(json);
286 290 break;
287 291 case 'error':
288 292 this.append_error(json);
289 293 break;
290 294 case 'display_data':
291 295 // append handled below
292 296 json = this.validate_mimebundle(json);
293 297 break;
294 298 default:
295 299 console.log("unrecognized output type: " + json.output_type);
296 300 this.append_unrecognized(json);
297 301 }
298 302
299 303 // We must release the animation fixed height in a callback since Gecko
300 304 // (FireFox) doesn't render the image immediately as the data is
301 305 // available.
302 306 var that = this;
303 307 var handle_appended = function ($el) {
304 308 /**
305 309 * Only reset the height to automatic if the height is currently
306 310 * fixed (done by wait=True flag on clear_output).
307 311 */
308 312 if (needs_height_reset) {
309 313 that.element.height('');
310 314 }
311 315 that.element.trigger('resize');
312 316 };
313 317 if (json.output_type === 'display_data') {
314 318 this.append_display_data(json, handle_appended);
315 319 } else {
316 320 handle_appended();
317 321 }
318 322
319 323 if (record_output) {
320 324 this.outputs.push(json);
321 325 }
322 326 };
323 327
324 328
325 329 OutputArea.prototype.create_output_area = function () {
326 330 var oa = $("<div/>").addClass("output_area");
327 331 if (this.prompt_area) {
328 332 oa.append($('<div/>').addClass('prompt'));
329 333 }
330 334 return oa;
331 335 };
332 336
333 337
334 338 function _get_metadata_key(metadata, key, mime) {
335 339 var mime_md = metadata[mime];
336 340 // mime-specific higher priority
337 341 if (mime_md && mime_md[key] !== undefined) {
338 342 return mime_md[key];
339 343 }
340 344 // fallback on global
341 345 return metadata[key];
342 346 }
343 347
344 348 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
345 349 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
346 350 if (_get_metadata_key(md, 'isolated', mime)) {
347 351 // Create an iframe to isolate the subarea from the rest of the
348 352 // document
349 353 var iframe = $('<iframe/>').addClass('box-flex1');
350 354 iframe.css({'height':1, 'width':'100%', 'display':'block'});
351 355 iframe.attr('frameborder', 0);
352 356 iframe.attr('scrolling', 'auto');
353 357
354 358 // Once the iframe is loaded, the subarea is dynamically inserted
355 359 iframe.on('load', function() {
356 360 // Workaround needed by Firefox, to properly render svg inside
357 361 // iframes, see http://stackoverflow.com/questions/10177190/
358 362 // svg-dynamically-added-to-iframe-does-not-render-correctly
359 363 this.contentDocument.open();
360 364
361 365 // Insert the subarea into the iframe
362 366 // We must directly write the html. When using Jquery's append
363 367 // method, javascript is evaluated in the parent document and
364 368 // not in the iframe document. At this point, subarea doesn't
365 369 // contain any user content.
366 370 this.contentDocument.write(subarea.html());
367 371
368 372 this.contentDocument.close();
369 373
370 374 var body = this.contentDocument.body;
371 375 // Adjust the iframe height automatically
372 376 iframe.height(body.scrollHeight + 'px');
373 377 });
374 378
375 379 // Elements should be appended to the inner subarea and not to the
376 380 // iframe
377 381 iframe.append = function(that) {
378 382 subarea.append(that);
379 383 };
380 384
381 385 return iframe;
382 386 } else {
383 387 return subarea;
384 388 }
385 389 };
386 390
387 391
388 392 OutputArea.prototype._append_javascript_error = function (err, element) {
389 393 /**
390 394 * display a message when a javascript error occurs in display output
391 395 */
392 396 var msg = "Javascript error adding output!";
393 397 if ( element === undefined ) return;
394 398 element
395 399 .append($('<div/>').text(msg).addClass('js-error'))
396 400 .append($('<div/>').text(err.toString()).addClass('js-error'))
397 401 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
398 402 };
399 403
400 404 OutputArea.prototype._safe_append = function (toinsert) {
401 405 /**
402 406 * safely append an item to the document
403 407 * this is an object created by user code,
404 408 * and may have errors, which should not be raised
405 409 * under any circumstances.
406 410 */
407 411 try {
408 412 this.element.append(toinsert);
409 413 } catch(err) {
410 414 console.log(err);
411 415 // Create an actual output_area and output_subarea, which creates
412 416 // the prompt area and the proper indentation.
413 417 var toinsert = this.create_output_area();
414 418 var subarea = $('<div/>').addClass('output_subarea');
415 419 toinsert.append(subarea);
416 420 this._append_javascript_error(err, subarea);
417 421 this.element.append(toinsert);
418 422 }
419 423
420 424 // Notify others of changes.
421 425 this.element.trigger('changed');
422 426 };
423 427
424 428
425 429 OutputArea.prototype.append_execute_result = function (json) {
426 430 var n = json.execution_count || ' ';
427 431 var toinsert = this.create_output_area();
428 432 if (this.prompt_area) {
429 433 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
430 434 }
431 435 var inserted = this.append_mime_type(json, toinsert);
432 436 if (inserted) {
433 437 inserted.addClass('output_result');
434 438 }
435 439 this._safe_append(toinsert);
436 440 // If we just output latex, typeset it.
437 441 if ((json.data['text/latex'] !== undefined) ||
438 442 (json.data['text/html'] !== undefined) ||
439 443 (json.data['text/markdown'] !== undefined)) {
440 444 this.typeset();
441 445 }
442 446 };
443 447
444 448
445 449 OutputArea.prototype.append_error = function (json) {
446 450 var tb = json.traceback;
447 451 if (tb !== undefined && tb.length > 0) {
448 452 var s = '';
449 453 var len = tb.length;
450 454 for (var i=0; i<len; i++) {
451 455 s = s + tb[i] + '\n';
452 456 }
453 457 s = s + '\n';
454 458 var toinsert = this.create_output_area();
455 459 var append_text = OutputArea.append_map['text/plain'];
456 460 if (append_text) {
457 461 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
458 462 }
459 463 this._safe_append(toinsert);
460 464 }
461 465 };
462 466
463 467
464 468 OutputArea.prototype.append_stream = function (json) {
465 469 var text = json.text;
466 470 if (typeof text !== 'string') {
467 471 console.error("Stream output is invalid (missing text)", json);
468 472 return false;
469 473 }
470 474 var subclass = "output_"+json.name;
471 475 if (this.outputs.length > 0){
472 476 // have at least one output to consider
473 477 var last = this.outputs[this.outputs.length-1];
474 478 if (last.output_type == 'stream' && json.name == last.name){
475 479 // latest output was in the same stream,
476 480 // so append directly into its pre tag
477 481 // escape ANSI & HTML specials:
478 482 last.text = utils.fixCarriageReturn(last.text + json.text);
479 483 var pre = this.element.find('div.'+subclass).last().find('pre');
480 484 var html = utils.fixConsole(last.text);
481 485 // The only user content injected with this HTML call is
482 486 // escaped by the fixConsole() method.
483 487 pre.html(html);
484 488 // return false signals that we merged this output with the previous one,
485 489 // and the new output shouldn't be recorded.
486 490 return false;
487 491 }
488 492 }
489 493
490 494 if (!text.replace("\r", "")) {
491 495 // text is nothing (empty string, \r, etc.)
492 496 // so don't append any elements, which might add undesirable space
493 497 // return true to indicate the output should be recorded.
494 498 return true;
495 499 }
496 500
497 501 // If we got here, attach a new div
498 502 var toinsert = this.create_output_area();
499 503 var append_text = OutputArea.append_map['text/plain'];
500 504 if (append_text) {
501 505 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
502 506 }
503 507 this._safe_append(toinsert);
504 508 return true;
505 509 };
506 510
507 511
508 512 OutputArea.prototype.append_unrecognized = function (json) {
509 513 var that = this;
510 514 var toinsert = this.create_output_area();
511 515 var subarea = $('<div/>').addClass('output_subarea output_unrecognized');
512 516 toinsert.append(subarea);
513 517 subarea.append(
514 518 $("<a>")
515 519 .attr("href", "#")
516 520 .text("Unrecognized output: " + json.output_type)
517 521 .click(function () {
518 that.events.trigger('unrecognized_output.OutputArea', {output: json})
522 that.events.trigger('unrecognized_output.OutputArea', {output: json});
519 523 })
520 524 );
521 525 this._safe_append(toinsert);
522 526 };
523 527
524 528
525 529 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
526 530 var toinsert = this.create_output_area();
527 531 if (this.append_mime_type(json, toinsert, handle_inserted)) {
528 532 this._safe_append(toinsert);
529 533 // If we just output latex, typeset it.
530 534 if ((json.data['text/latex'] !== undefined) ||
531 535 (json.data['text/html'] !== undefined) ||
532 536 (json.data['text/markdown'] !== undefined)) {
533 537 this.typeset();
534 538 }
535 539 }
536 540 };
537 541
538 542
539 543 OutputArea.safe_outputs = {
540 544 'text/plain' : true,
541 545 'text/latex' : true,
542 546 'image/png' : true,
543 547 'image/jpeg' : true
544 548 };
545 549
546 550 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
547 551 for (var i=0; i < OutputArea.display_order.length; i++) {
548 552 var type = OutputArea.display_order[i];
549 553 var append = OutputArea.append_map[type];
550 554 if ((json.data[type] !== undefined) && append) {
551 555 var value = json.data[type];
552 556 if (!this.trusted && !OutputArea.safe_outputs[type]) {
553 557 // not trusted, sanitize HTML
554 558 if (type==='text/html' || type==='text/svg') {
555 559 value = security.sanitize_html(value);
556 560 } else {
557 561 // don't display if we don't know how to sanitize it
558 562 console.log("Ignoring untrusted " + type + " output.");
559 563 continue;
560 564 }
561 565 }
562 566 var md = json.metadata || {};
563 567 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
564 568 // Since only the png and jpeg mime types call the inserted
565 569 // callback, if the mime type is something other we must call the
566 570 // inserted callback only when the element is actually inserted
567 571 // into the DOM. Use a timeout of 0 to do this.
568 572 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
569 573 setTimeout(handle_inserted, 0);
570 574 }
571 575 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
572 576 return toinsert;
573 577 }
574 578 }
575 579 return null;
576 580 };
577 581
578 582
579 583 var append_html = function (html, md, element) {
580 584 var type = 'text/html';
581 585 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
582 586 this.keyboard_manager.register_events(toinsert);
583 587 toinsert.append(html);
584 588 element.append(toinsert);
585 589 return toinsert;
586 590 };
587 591
588 592
589 593 var append_markdown = function(markdown, md, element) {
590 594 var type = 'text/markdown';
591 595 var toinsert = this.create_output_subarea(md, "output_markdown", type);
592 596 var text_and_math = mathjaxutils.remove_math(markdown);
593 597 var text = text_and_math[0];
594 598 var math = text_and_math[1];
595 599 marked(text, function (err, html) {
596 600 html = mathjaxutils.replace_math(html, math);
597 601 toinsert.append(html);
598 602 });
599 603 element.append(toinsert);
600 604 return toinsert;
601 605 };
602 606
603 607
604 608 var append_javascript = function (js, md, element) {
605 609 /**
606 610 * We just eval the JS code, element appears in the local scope.
607 611 */
608 612 var type = 'application/javascript';
609 613 var toinsert = this.create_output_subarea(md, "output_javascript", type);
610 614 this.keyboard_manager.register_events(toinsert);
611 615 element.append(toinsert);
612 616
613 617 // Fix for ipython/issues/5293, make sure `element` is the area which
614 618 // output can be inserted into at the time of JS execution.
615 619 element = toinsert;
616 620 try {
617 621 eval(js);
618 622 } catch(err) {
619 623 console.log(err);
620 624 this._append_javascript_error(err, toinsert);
621 625 }
622 626 return toinsert;
623 627 };
624 628
625 629
626 630 var append_text = function (data, md, element) {
627 631 var type = 'text/plain';
628 632 var toinsert = this.create_output_subarea(md, "output_text", type);
629 633 // escape ANSI & HTML specials in plaintext:
630 634 data = utils.fixConsole(data);
631 635 data = utils.fixCarriageReturn(data);
632 636 data = utils.autoLinkUrls(data);
633 637 // The only user content injected with this HTML call is
634 638 // escaped by the fixConsole() method.
635 639 toinsert.append($("<pre/>").html(data));
636 640 element.append(toinsert);
637 641 return toinsert;
638 642 };
639 643
640 644
641 645 var append_svg = function (svg_html, md, element) {
642 646 var type = 'image/svg+xml';
643 647 var toinsert = this.create_output_subarea(md, "output_svg", type);
644 648
645 649 // Get the svg element from within the HTML.
646 650 var svg = $('<div />').html(svg_html).find('svg');
647 651 var svg_area = $('<div />');
648 652 var width = svg.attr('width');
649 653 var height = svg.attr('height');
650 654 svg
651 655 .width('100%')
652 656 .height('100%');
653 657 svg_area
654 658 .width(width)
655 659 .height(height);
656 660
657 661 // The jQuery resize handlers don't seem to work on the svg element.
658 662 // When the svg renders completely, measure it's size and set the parent
659 663 // div to that size. Then set the svg to 100% the size of the parent
660 664 // div and make the parent div resizable.
661 665 this._dblclick_to_reset_size(svg_area, true, false);
662 666
663 667 svg_area.append(svg);
664 668 toinsert.append(svg_area);
665 669 element.append(toinsert);
666 670
667 671 return toinsert;
668 672 };
669 673
670 674 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
671 675 /**
672 676 * Add a resize handler to an element
673 677 *
674 678 * img: jQuery element
675 679 * immediately: bool=False
676 680 * Wait for the element to load before creating the handle.
677 681 * resize_parent: bool=True
678 682 * Should the parent of the element be resized when the element is
679 683 * reset (by double click).
680 684 */
681 685 var callback = function (){
682 686 var h0 = img.height();
683 687 var w0 = img.width();
684 688 if (!(h0 && w0)) {
685 689 // zero size, don't make it resizable
686 690 return;
687 691 }
688 692 img.resizable({
689 693 aspectRatio: true,
690 694 autoHide: true
691 695 });
692 696 img.dblclick(function () {
693 697 // resize wrapper & image together for some reason:
694 698 img.height(h0);
695 699 img.width(w0);
696 700 if (resize_parent === undefined || resize_parent) {
697 701 img.parent().height(h0);
698 702 img.parent().width(w0);
699 703 }
700 704 });
701 705 };
702 706
703 707 if (immediately) {
704 708 callback();
705 709 } else {
706 710 img.on("load", callback);
707 711 }
708 712 };
709 713
710 714 var set_width_height = function (img, md, mime) {
711 715 /**
712 716 * set width and height of an img element from metadata
713 717 */
714 718 var height = _get_metadata_key(md, 'height', mime);
715 719 if (height !== undefined) img.attr('height', height);
716 720 var width = _get_metadata_key(md, 'width', mime);
717 721 if (width !== undefined) img.attr('width', width);
718 722 };
719 723
720 724 var append_png = function (png, md, element, handle_inserted) {
721 725 var type = 'image/png';
722 726 var toinsert = this.create_output_subarea(md, "output_png", type);
723 727 var img = $("<img/>");
724 728 if (handle_inserted !== undefined) {
725 729 img.on('load', function(){
726 730 handle_inserted(img);
727 731 });
728 732 }
729 733 img[0].src = 'data:image/png;base64,'+ png;
730 734 set_width_height(img, md, 'image/png');
731 735 this._dblclick_to_reset_size(img);
732 736 toinsert.append(img);
733 737 element.append(toinsert);
734 738 return toinsert;
735 739 };
736 740
737 741
738 742 var append_jpeg = function (jpeg, md, element, handle_inserted) {
739 743 var type = 'image/jpeg';
740 744 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
741 745 var img = $("<img/>");
742 746 if (handle_inserted !== undefined) {
743 747 img.on('load', function(){
744 748 handle_inserted(img);
745 749 });
746 750 }
747 751 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
748 752 set_width_height(img, md, 'image/jpeg');
749 753 this._dblclick_to_reset_size(img);
750 754 toinsert.append(img);
751 755 element.append(toinsert);
752 756 return toinsert;
753 757 };
754 758
755 759
756 760 var append_pdf = function (pdf, md, element) {
757 761 var type = 'application/pdf';
758 762 var toinsert = this.create_output_subarea(md, "output_pdf", type);
759 763 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
760 764 a.attr('target', '_blank');
761 765 a.text('View PDF');
762 766 toinsert.append(a);
763 767 element.append(toinsert);
764 768 return toinsert;
765 769 };
766 770
767 771 var append_latex = function (latex, md, element) {
768 772 /**
769 773 * This method cannot do the typesetting because the latex first has to
770 774 * be on the page.
771 775 */
772 776 var type = 'text/latex';
773 777 var toinsert = this.create_output_subarea(md, "output_latex", type);
774 778 toinsert.append(latex);
775 779 element.append(toinsert);
776 780 return toinsert;
777 781 };
778 782
779 783
780 784 OutputArea.prototype.append_raw_input = function (msg) {
781 785 var that = this;
782 786 this.expand();
783 787 var content = msg.content;
784 788 var area = this.create_output_area();
785 789
786 790 // disable any other raw_inputs, if they are left around
787 791 $("div.output_subarea.raw_input_container").remove();
788 792
789 793 var input_type = content.password ? 'password' : 'text';
790 794
791 795 area.append(
792 796 $("<div/>")
793 797 .addClass("box-flex1 output_subarea raw_input_container")
794 798 .append(
795 799 $("<span/>")
796 800 .addClass("raw_input_prompt")
797 801 .text(content.prompt)
798 802 )
799 803 .append(
800 804 $("<input/>")
801 805 .addClass("raw_input")
802 806 .attr('type', input_type)
803 807 .attr("size", 47)
804 808 .keydown(function (event, ui) {
805 809 // make sure we submit on enter,
806 810 // and don't re-execute the *cell* on shift-enter
807 811 if (event.which === keyboard.keycodes.enter) {
808 812 that._submit_raw_input();
809 813 return false;
810 814 }
811 815 })
812 816 )
813 817 );
814 818
815 819 this.element.append(area);
816 820 var raw_input = area.find('input.raw_input');
817 821 // Register events that enable/disable the keyboard manager while raw
818 822 // input is focused.
819 823 this.keyboard_manager.register_events(raw_input);
820 824 // Note, the following line used to read raw_input.focus().focus().
821 825 // This seemed to be needed otherwise only the cell would be focused.
822 826 // But with the modal UI, this seems to work fine with one call to focus().
823 827 raw_input.focus();
824 828 };
825 829
826 830 OutputArea.prototype._submit_raw_input = function (evt) {
827 831 var container = this.element.find("div.raw_input_container");
828 832 var theprompt = container.find("span.raw_input_prompt");
829 833 var theinput = container.find("input.raw_input");
830 834 var value = theinput.val();
831 835 var echo = value;
832 836 // don't echo if it's a password
833 837 if (theinput.attr('type') == 'password') {
834 838 echo = 'Β·Β·Β·Β·Β·Β·Β·Β·';
835 839 }
836 840 var content = {
837 841 output_type : 'stream',
838 842 name : 'stdout',
839 843 text : theprompt.text() + echo + '\n'
840 844 };
841 845 // remove form container
842 846 container.parent().remove();
843 847 // replace with plaintext version in stdout
844 848 this.append_output(content, false);
845 849 this.events.trigger('send_input_reply.Kernel', value);
846 850 };
847 851
848 852
849 853 OutputArea.prototype.handle_clear_output = function (msg) {
850 854 /**
851 855 * msg spec v4 had stdout, stderr, display keys
852 856 * v4.1 replaced these with just wait
853 857 * The default behavior is the same (stdout=stderr=display=True, wait=False),
854 858 * so v4 messages will still be properly handled,
855 859 * except for the rarely used clearing less than all output.
856 860 */
857 861 this.clear_output(msg.content.wait || false);
858 862 };
859 863
860 864
861 865 OutputArea.prototype.clear_output = function(wait, ignore_que) {
862 866 if (wait) {
863 867
864 868 // If a clear is queued, clear before adding another to the queue.
865 869 if (this.clear_queued) {
866 870 this.clear_output(false);
867 871 }
868 872
869 873 this.clear_queued = true;
870 874 } else {
871 875
872 876 // Fix the output div's height if the clear_output is waiting for
873 877 // new output (it is being used in an animation).
874 878 if (!ignore_que && this.clear_queued) {
875 879 var height = this.element.height();
876 880 this.element.height(height);
877 881 this.clear_queued = false;
878 882 }
879 883
880 884 // Clear all
881 885 // Remove load event handlers from img tags because we don't want
882 886 // them to fire if the image is never added to the page.
883 887 this.element.find('img').off('load');
884 888 this.element.html("");
885 889
886 890 // Notify others of changes.
887 891 this.element.trigger('changed');
888 892
889 893 this.outputs = [];
890 894 this.trusted = true;
891 895 this.unscroll_area();
892 896 return;
893 897 }
894 898 };
895 899
896 900
897 901 // JSON serialization
898 902
899 903 OutputArea.prototype.fromJSON = function (outputs, metadata) {
900 904 var len = outputs.length;
901 905 metadata = metadata || {};
902 906
903 907 for (var i=0; i<len; i++) {
904 908 this.append_output(outputs[i]);
905 909 }
906
907 910 if (metadata.collapsed !== undefined) {
908 911 this.collapsed = metadata.collapsed;
909 912 if (metadata.collapsed) {
910 this.collapse_output();
913 this.collapse();
911 914 }
912 915 }
913 if (metadata.autoscroll !== undefined) {
914 this.collapsed = metadata.collapsed;
915 if (metadata.collapsed) {
916 this.collapse_output();
916 if (metadata.scrolled !== undefined) {
917 this.scroll_state = metadata.scrolled;
918 if (metadata.scrolled) {
919 this.scroll_if_long();
917 920 } else {
918 this.expand_output();
921 this.unscroll_area();
919 922 }
920 923 }
921 924 };
922 925
923 926
924 927 OutputArea.prototype.toJSON = function () {
925 928 return this.outputs;
926 929 };
927 930
928 931 /**
929 932 * Class properties
930 933 **/
931 934
932 935 /**
933 936 * Threshold to trigger autoscroll when the OutputArea is resized,
934 937 * typically when new outputs are added.
935 938 *
936 939 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
937 940 * unless it is < 0, in which case autoscroll will never be triggered
938 941 *
939 942 * @property auto_scroll_threshold
940 943 * @type Number
941 944 * @default 100
942 945 *
943 946 **/
944 947 OutputArea.auto_scroll_threshold = 100;
945 948
946 949 /**
947 950 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
948 951 * shorter than this are never scrolled.
949 952 *
950 953 * @property minimum_scroll_threshold
951 954 * @type Number
952 955 * @default 20
953 956 *
954 957 **/
955 958 OutputArea.minimum_scroll_threshold = 20;
956 959
957 960
958 961 OutputArea.display_order = [
959 962 'application/javascript',
960 963 'text/html',
961 964 'text/markdown',
962 965 'text/latex',
963 966 'image/svg+xml',
964 967 'image/png',
965 968 'image/jpeg',
966 969 'application/pdf',
967 970 'text/plain'
968 971 ];
969 972
970 973 OutputArea.append_map = {
971 974 "text/plain" : append_text,
972 975 "text/html" : append_html,
973 976 "text/markdown": append_markdown,
974 977 "image/svg+xml" : append_svg,
975 978 "image/png" : append_png,
976 979 "image/jpeg" : append_jpeg,
977 980 "text/latex" : append_latex,
978 981 "application/javascript" : append_javascript,
979 982 "application/pdf" : append_pdf
980 983 };
981 984
982 985 // For backwards compatability.
983 986 IPython.OutputArea = OutputArea;
984 987
985 988 return {'OutputArea': OutputArea};
986 989 });
@@ -1,371 +1,371 b''
1 1 {
2 2 "$schema": "http://json-schema.org/draft-04/schema#",
3 3 "description": "IPython Notebook v4.0 JSON schema.",
4 4 "type": "object",
5 5 "additionalProperties": false,
6 6 "required": ["metadata", "nbformat_minor", "nbformat", "cells"],
7 7 "properties": {
8 8 "metadata": {
9 9 "description": "Notebook root-level metadata.",
10 10 "type": "object",
11 11 "additionalProperties": true,
12 12 "properties": {
13 13 "kernelspec": {
14 14 "description": "Kernel information.",
15 15 "type": "object",
16 16 "required": ["name", "display_name"],
17 17 "properties": {
18 18 "name": {
19 19 "description": "Name of the kernel specification.",
20 20 "type": "string"
21 21 },
22 22 "display_name": {
23 23 "description": "Name to display in UI.",
24 24 "type": "string"
25 25 }
26 26 }
27 27 },
28 28 "language_info": {
29 29 "description": "Kernel information.",
30 30 "type": "object",
31 31 "required": ["name"],
32 32 "properties": {
33 33 "name": {
34 34 "description": "The programming language which this kernel runs.",
35 35 "type": "string"
36 36 },
37 37 "codemirror_mode": {
38 38 "description": "The codemirror mode to use for code in this language.",
39 39 "oneOf": [
40 40 {"type": "string"},
41 41 {"type": "object"}
42 42 ]
43 43 },
44 44 "file_extension": {
45 45 "description": "The file extension for files in this language.",
46 46 "type": "string"
47 47 },
48 48 "mimetype": {
49 49 "description": "The mimetype corresponding to files in this language.",
50 50 "type": "string"
51 51 },
52 52 "pygments_lexer": {
53 53 "description": "The pygments lexer to use for code in this language.",
54 54 "type": "string"
55 55 }
56 56 }
57 57 },
58 58 "orig_nbformat": {
59 59 "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.",
60 60 "type": "integer",
61 61 "minimum": 1
62 62 }
63 63 }
64 64 },
65 65 "nbformat_minor": {
66 66 "description": "Notebook format (minor number). Incremented for backward compatible changes to the notebook format.",
67 67 "type": "integer",
68 68 "minimum": 0
69 69 },
70 70 "nbformat": {
71 71 "description": "Notebook format (major number). Incremented between backwards incompatible changes to the notebook format.",
72 72 "type": "integer",
73 73 "minimum": 4,
74 74 "maximum": 4
75 75 },
76 76 "cells": {
77 77 "description": "Array of cells of the current notebook.",
78 78 "type": "array",
79 79 "items": {"$ref": "#/definitions/cell"}
80 80 }
81 81 },
82 82
83 83 "definitions": {
84 84 "cell": {
85 85 "type": "object",
86 86 "oneOf": [
87 87 {"$ref": "#/definitions/raw_cell"},
88 88 {"$ref": "#/definitions/markdown_cell"},
89 89 {"$ref": "#/definitions/code_cell"}
90 90 ]
91 91 },
92 92
93 93 "raw_cell": {
94 94 "description": "Notebook raw nbconvert cell.",
95 95 "type": "object",
96 96 "additionalProperties": false,
97 97 "required": ["cell_type", "metadata", "source"],
98 98 "properties": {
99 99 "cell_type": {
100 100 "description": "String identifying the type of cell.",
101 101 "enum": ["raw"]
102 102 },
103 103 "metadata": {
104 104 "description": "Cell-level metadata.",
105 105 "type": "object",
106 106 "additionalProperties": true,
107 107 "properties": {
108 108 "format": {
109 109 "description": "Raw cell metadata format for nbconvert.",
110 110 "type": "string"
111 111 },
112 112 "name": {"$ref": "#/definitions/misc/metadata_name"},
113 113 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
114 114 }
115 115 },
116 116 "source": {"$ref": "#/definitions/misc/source"}
117 117 }
118 118 },
119 119
120 120 "markdown_cell": {
121 121 "description": "Notebook markdown cell.",
122 122 "type": "object",
123 123 "additionalProperties": false,
124 124 "required": ["cell_type", "metadata", "source"],
125 125 "properties": {
126 126 "cell_type": {
127 127 "description": "String identifying the type of cell.",
128 128 "enum": ["markdown"]
129 129 },
130 130 "metadata": {
131 131 "description": "Cell-level metadata.",
132 132 "type": "object",
133 133 "properties": {
134 134 "name": {"$ref": "#/definitions/misc/metadata_name"},
135 135 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
136 136 },
137 137 "additionalProperties": true
138 138 },
139 139 "source": {"$ref": "#/definitions/misc/source"}
140 140 }
141 141 },
142 142
143 143 "code_cell": {
144 144 "description": "Notebook code cell.",
145 145 "type": "object",
146 146 "additionalProperties": false,
147 147 "required": ["cell_type", "metadata", "source", "outputs", "execution_count"],
148 148 "properties": {
149 149 "cell_type": {
150 150 "description": "String identifying the type of cell.",
151 151 "enum": ["code"]
152 152 },
153 153 "metadata": {
154 154 "description": "Cell-level metadata.",
155 155 "type": "object",
156 156 "additionalProperties": true,
157 157 "properties": {
158 158 "collapsed": {
159 159 "description": "Whether the cell is collapsed/expanded.",
160 160 "type": "boolean"
161 161 },
162 "autoscroll": {
162 "scrolled": {
163 163 "description": "Whether the cell's output is scrolled, unscrolled, or autoscrolled.",
164 164 "enum": [true, false, "auto"]
165 165 },
166 166 "name": {"$ref": "#/definitions/misc/metadata_name"},
167 167 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
168 168 }
169 169 },
170 170 "source": {"$ref": "#/definitions/misc/source"},
171 171 "outputs": {
172 172 "description": "Execution, display, or stream outputs.",
173 173 "type": "array",
174 174 "items": {"$ref": "#/definitions/output"}
175 175 },
176 176 "execution_count": {
177 177 "description": "The code cell's prompt number. Will be null if the cell has not been run.",
178 178 "type": ["integer", "null"],
179 179 "minimum": 0
180 180 }
181 181 }
182 182 },
183 183
184 184 "unrecognized_cell": {
185 185 "description": "Unrecognized cell from a future minor-revision to the notebook format.",
186 186 "type": "object",
187 187 "additionalProperties": true,
188 188 "required": ["cell_type", "metadata"],
189 189 "properties": {
190 190 "cell_type": {
191 191 "description": "String identifying the type of cell.",
192 192 "not" : {
193 193 "enum": ["markdown", "code", "raw"]
194 194 }
195 195 },
196 196 "metadata": {
197 197 "description": "Cell-level metadata.",
198 198 "type": "object",
199 199 "properties": {
200 200 "name": {"$ref": "#/definitions/misc/metadata_name"},
201 201 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
202 202 },
203 203 "additionalProperties": true
204 204 }
205 205 }
206 206 },
207 207
208 208 "output": {
209 209 "type": "object",
210 210 "oneOf": [
211 211 {"$ref": "#/definitions/execute_result"},
212 212 {"$ref": "#/definitions/display_data"},
213 213 {"$ref": "#/definitions/stream"},
214 214 {"$ref": "#/definitions/error"}
215 215 ]
216 216 },
217 217
218 218 "execute_result": {
219 219 "description": "Result of executing a code cell.",
220 220 "type": "object",
221 221 "additionalProperties": false,
222 222 "required": ["output_type", "data", "metadata", "execution_count"],
223 223 "properties": {
224 224 "output_type": {
225 225 "description": "Type of cell output.",
226 226 "enum": ["execute_result"]
227 227 },
228 228 "execution_count": {
229 229 "description": "A result's prompt number.",
230 230 "type": ["integer", "null"],
231 231 "minimum": 0
232 232 },
233 233 "data": {"$ref": "#/definitions/misc/mimebundle"},
234 234 "metadata": {"$ref": "#/definitions/misc/output_metadata"}
235 235 }
236 236 },
237 237
238 238 "display_data": {
239 239 "description": "Data displayed as a result of code cell execution.",
240 240 "type": "object",
241 241 "additionalProperties": false,
242 242 "required": ["output_type", "data", "metadata"],
243 243 "properties": {
244 244 "output_type": {
245 245 "description": "Type of cell output.",
246 246 "enum": ["display_data"]
247 247 },
248 248 "data": {"$ref": "#/definitions/misc/mimebundle"},
249 249 "metadata": {"$ref": "#/definitions/misc/output_metadata"}
250 250 }
251 251 },
252 252
253 253 "stream": {
254 254 "description": "Stream output from a code cell.",
255 255 "type": "object",
256 256 "additionalProperties": false,
257 257 "required": ["output_type", "name", "text"],
258 258 "properties": {
259 259 "output_type": {
260 260 "description": "Type of cell output.",
261 261 "enum": ["stream"]
262 262 },
263 263 "name": {
264 264 "description": "The name of the stream (stdout, stderr).",
265 265 "type": "string"
266 266 },
267 267 "text": {
268 268 "description": "The stream's text output, represented as an array of strings.",
269 269 "$ref": "#/definitions/misc/multiline_string"
270 270 }
271 271 }
272 272 },
273 273
274 274 "error": {
275 275 "description": "Output of an error that occurred during code cell execution.",
276 276 "type": "object",
277 277 "additionalProperties": false,
278 278 "required": ["output_type", "ename", "evalue", "traceback"],
279 279 "properties": {
280 280 "output_type": {
281 281 "description": "Type of cell output.",
282 282 "enum": ["error"]
283 283 },
284 284 "ename": {
285 285 "description": "The name of the error.",
286 286 "type": "string"
287 287 },
288 288 "evalue": {
289 289 "description": "The value, or message, of the error.",
290 290 "type": "string"
291 291 },
292 292 "traceback": {
293 293 "description": "The error's traceback, represented as an array of strings.",
294 294 "type": "array",
295 295 "items": {"type": "string"}
296 296 }
297 297 }
298 298 },
299 299
300 300 "unrecognized_output": {
301 301 "description": "Unrecognized output from a future minor-revision to the notebook format.",
302 302 "type": "object",
303 303 "additionalProperties": true,
304 304 "required": ["output_type"],
305 305 "properties": {
306 306 "output_type": {
307 307 "description": "Type of cell output.",
308 308 "not": {
309 309 "enum": ["execute_result", "display_data", "stream", "error"]
310 310 }
311 311 }
312 312 }
313 313 },
314 314
315 315 "misc": {
316 316 "metadata_name": {
317 317 "description": "The cell's name. If present, must be a non-empty string.",
318 318 "type": "string",
319 319 "pattern": "^.+$"
320 320 },
321 321 "metadata_tags": {
322 322 "description": "The cell's tags. Tags must be unique, and must not contain commas.",
323 323 "type": "array",
324 324 "uniqueItems": true,
325 325 "items": {
326 326 "type": "string",
327 327 "pattern": "^[^,]+$"
328 328 }
329 329 },
330 330 "source": {
331 331 "description": "Contents of the cell, represented as an array of lines.",
332 332 "$ref": "#/definitions/misc/multiline_string"
333 333 },
334 334 "execution_count": {
335 335 "description": "The code cell's prompt number. Will be null if the cell has not been run.",
336 336 "type": ["integer", "null"],
337 337 "minimum": 0
338 338 },
339 339 "mimebundle": {
340 340 "description": "A mime-type keyed dictionary of data",
341 341 "type": "object",
342 342 "additionalProperties": false,
343 343 "properties": {
344 344 "application/json": {
345 345 "type": "object"
346 346 }
347 347 },
348 348 "patternProperties": {
349 349 "^(?!application/json$)[a-zA-Z0-9]+/[a-zA-Z0-9\\-\\+\\.]+$": {
350 350 "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.",
351 351 "$ref": "#/definitions/misc/multiline_string"
352 352 }
353 353 }
354 354 },
355 355 "output_metadata": {
356 356 "description": "Cell output metadata.",
357 357 "type": "object",
358 358 "additionalProperties": true
359 359 },
360 360 "multiline_string": {
361 361 "oneOf" : [
362 362 {"type": "string"},
363 363 {
364 364 "type": "array",
365 365 "items": {"type": "string"}
366 366 }
367 367 ]
368 368 }
369 369 }
370 370 }
371 371 }
General Comments 0
You need to be logged in to leave comments. Login now