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