##// END OF EJS Templates
Merge pull request #7523 from jdfreder/outputwidgetfix...
Min RK -
r20096:5616d61c merge
parent child Browse files
Show More
@@ -1,677 +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 // Keep a stack of the 'active' output areas (where active means the
101 // output area that recieves output). When a user activates an output
102 // area, it gets pushed to the stack. Then, when the output area is
103 // deactivated, it's popped from the stack. When the stack is empty,
104 // the cell's output area is used.
105 this.active_output_areas = [];
106 var that = this;
107 Object.defineProperty(this, 'active_output_area', {
108 get: function() {
109 if (that.active_output_areas && that.active_output_areas.length > 0) {
110 return that.active_output_areas[that.active_output_areas.length-1];
111 } else {
112 return that.output_area;
113 }
114 },
115 });
116 100
117 101 this.last_msg_id = null;
118 102 this.completer = null;
119 103 this.widget_views = [];
120 104 this._widgets_live = true;
121 105
122 106 Cell.apply(this,[{
123 107 config: $.extend({}, CodeCell.options_default),
124 108 keyboard_manager: options.keyboard_manager,
125 109 events: this.events}]);
126 110
127 111 // Attributes we want to override in this subclass.
128 112 this.cell_type = "code";
113 var that = this;
129 114 this.element.focusout(
130 115 function() { that.auto_highlight(); }
131 116 );
132 117 };
133 118
134 119 CodeCell.options_default = {
135 120 cm_config : {
136 121 extraKeys: {
137 122 "Tab" : "indentMore",
138 123 "Shift-Tab" : "indentLess",
139 124 "Backspace" : "delSpaceToPrevTabStop",
140 125 "Cmd-/" : "toggleComment",
141 126 "Ctrl-/" : "toggleComment"
142 127 },
143 128 mode: 'ipython',
144 129 theme: 'ipython',
145 130 matchBrackets: true
146 131 }
147 132 };
148 133
149 134 CodeCell.config_defaults = {
150 135 highlight_modes : {
151 136 'magic_javascript' :{'reg':[/^%%javascript/]},
152 137 'magic_perl' :{'reg':[/^%%perl/]},
153 138 'magic_ruby' :{'reg':[/^%%ruby/]},
154 139 'magic_python' :{'reg':[/^%%python3?/]},
155 140 'magic_shell' :{'reg':[/^%%bash/]},
156 141 'magic_r' :{'reg':[/^%%R/]},
157 142 'magic_text/x-cython' :{'reg':[/^%%cython/]},
158 143 },
159 144 };
160 145
161 146 CodeCell.msg_cells = {};
162 147
163 148 CodeCell.prototype = Object.create(Cell.prototype);
164 149
165 /**
166 * @method push_output_area
167 */
168 CodeCell.prototype.push_output_area = function (output_area) {
169 this.active_output_areas.push(output_area);
170 };
171
172 /**
173 * @method pop_output_area
174 */
175 CodeCell.prototype.pop_output_area = function (output_area) {
176 var index = this.active_output_areas.lastIndexOf(output_area);
177 if (index > -1) {
178 this.active_output_areas.splice(index, 1);
179 }
180 };
181
182 150 /** @method create_element */
183 151 CodeCell.prototype.create_element = function () {
184 152 Cell.prototype.create_element.apply(this, arguments);
185 153
186 154 var cell = $('<div></div>').addClass('cell code_cell');
187 155 cell.attr('tabindex','2');
188 156
189 157 var input = $('<div></div>').addClass('input');
190 158 var prompt = $('<div/>').addClass('prompt input_prompt');
191 159 var inner_cell = $('<div/>').addClass('inner_cell');
192 160 this.celltoolbar = new celltoolbar.CellToolbar({
193 161 cell: this,
194 162 notebook: this.notebook});
195 163 inner_cell.append(this.celltoolbar.element);
196 164 var input_area = $('<div/>').addClass('input_area');
197 165 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
198 166 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this));
199 167 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
200 168 inner_cell.append(input_area);
201 169 input.append(prompt).append(inner_cell);
202 170
203 171 var widget_area = $('<div/>')
204 172 .addClass('widget-area')
205 173 .hide();
206 174 this.widget_area = widget_area;
207 175 var widget_prompt = $('<div/>')
208 176 .addClass('prompt')
209 177 .appendTo(widget_area);
210 178 var widget_subarea = $('<div/>')
211 179 .addClass('widget-subarea')
212 180 .appendTo(widget_area);
213 181 this.widget_subarea = widget_subarea;
214 182 var that = this;
215 183 var widget_clear_buton = $('<button />')
216 184 .addClass('close')
217 185 .html('&times;')
218 186 .click(function() {
219 187 widget_area.slideUp('', function(){
220 188 for (var i = 0; i < that.widget_views.length; i++) {
221 189 var view = that.widget_views[i];
222 190 view.remove();
223 191
224 192 // Remove widget live events.
225 193 view.off('comm:live', that._widget_live);
226 194 view.off('comm:dead', that._widget_dead);
227 195 }
228 196 that.widget_views = [];
229 197 widget_subarea.html('');
230 198 });
231 199 })
232 200 .appendTo(widget_prompt);
233 201
234 202 var output = $('<div></div>');
235 203 cell.append(input).append(widget_area).append(output);
236 204 this.element = cell;
237 205 this.output_area = new outputarea.OutputArea({
238 206 selector: output,
239 207 prompt_area: true,
240 208 events: this.events,
241 209 keyboard_manager: this.keyboard_manager});
242 210 this.completer = new completer.Completer(this, this.events);
243 211 };
244 212
245 213 /**
246 214 * Display a widget view in the cell.
247 215 */
248 216 CodeCell.prototype.display_widget_view = function(view_promise) {
249 217
250 218 // Display a dummy element
251 219 var dummy = $('<div/>');
252 220 this.widget_subarea.append(dummy);
253 221
254 222 // Display the view.
255 223 var that = this;
256 224 return view_promise.then(function(view) {
257 225 that.widget_area.show();
258 226 dummy.replaceWith(view.$el);
259 227 that.widget_views.push(view);
260 228
261 229 // Check the live state of the view's model.
262 230 if (view.model.comm_live) {
263 231 that._widget_live(view);
264 232 } else {
265 233 that._widget_dead(view);
266 234 }
267 235
268 236 // Listen to comm live events for the view.
269 237 view.on('comm:live', that._widget_live, that);
270 238 view.on('comm:dead', that._widget_dead, that);
271 239 return view;
272 240 });
273 241 };
274 242
275 243 /**
276 244 * Handles when a widget loses it's comm connection.
277 245 * @param {WidgetView} view
278 246 */
279 247 CodeCell.prototype._widget_dead = function(view) {
280 248 if (this._widgets_live) {
281 249 this._widgets_live = false;
282 250 this.widget_area.addClass('connection-problems');
283 251 }
284 252
285 253 };
286 254
287 255 /**
288 256 * Handles when a widget is connected to a live comm.
289 257 * @param {WidgetView} view
290 258 */
291 259 CodeCell.prototype._widget_live = function(view) {
292 260 if (!this._widgets_live) {
293 261 // Check that the other widgets are live too. O(N) operation.
294 262 // Abort the function at the first dead widget found.
295 263 for (var i = 0; i < this.widget_views.length; i++) {
296 264 if (!this.widget_views[i].model.comm_live) return;
297 265 }
298 266 this._widgets_live = true;
299 267 this.widget_area.removeClass('connection-problems');
300 268 }
301 269 };
302 270
303 271 /** @method bind_events */
304 272 CodeCell.prototype.bind_events = function () {
305 273 Cell.prototype.bind_events.apply(this);
306 274 var that = this;
307 275
308 276 this.element.focusout(
309 277 function() { that.auto_highlight(); }
310 278 );
311 279 };
312 280
313 281
314 282 /**
315 283 * This method gets called in CodeMirror's onKeyDown/onKeyPress
316 284 * handlers and is used to provide custom key handling. Its return
317 285 * value is used to determine if CodeMirror should ignore the event:
318 286 * true = ignore, false = don't ignore.
319 287 * @method handle_codemirror_keyevent
320 288 */
321 289
322 290 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
323 291
324 292 var that = this;
325 293 // whatever key is pressed, first, cancel the tooltip request before
326 294 // they are sent, and remove tooltip if any, except for tab again
327 295 var tooltip_closed = null;
328 296 if (event.type === 'keydown' && event.which !== keycodes.tab ) {
329 297 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
330 298 }
331 299
332 300 var cur = editor.getCursor();
333 301 if (event.keyCode === keycodes.enter){
334 302 this.auto_highlight();
335 303 }
336 304
337 305 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
338 306 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
339 307 // browser and keyboard layout !
340 308 // Pressing '(' , request tooltip, don't forget to reappend it
341 309 // The second argument says to hide the tooltip if the docstring
342 310 // is actually empty
343 311 this.tooltip.pending(that, true);
344 312 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
345 313 // If tooltip is active, cancel it. The call to
346 314 // remove_and_cancel_tooltip above doesn't pass, force=true.
347 315 // Because of this it won't actually close the tooltip
348 316 // if it is in sticky mode. Thus, we have to check again if it is open
349 317 // and close it with force=true.
350 318 if (!this.tooltip._hidden) {
351 319 this.tooltip.remove_and_cancel_tooltip(true);
352 320 }
353 321 // If we closed the tooltip, don't let CM or the global handlers
354 322 // handle this event.
355 323 event.codemirrorIgnore = true;
356 324 event.preventDefault();
357 325 return true;
358 326 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
359 327 if (editor.somethingSelected() || editor.getSelections().length !== 1){
360 328 var anchor = editor.getCursor("anchor");
361 329 var head = editor.getCursor("head");
362 330 if( anchor.line !== head.line){
363 331 return false;
364 332 }
365 333 }
366 334 this.tooltip.request(that);
367 335 event.codemirrorIgnore = true;
368 336 event.preventDefault();
369 337 return true;
370 338 } else if (event.keyCode === keycodes.tab && event.type === 'keydown') {
371 339 // Tab completion.
372 340 this.tooltip.remove_and_cancel_tooltip();
373 341
374 342 // completion does not work on multicursor, it might be possible though in some cases
375 343 if (editor.somethingSelected() || editor.getSelections().length > 1) {
376 344 return false;
377 345 }
378 346 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
379 347 if (pre_cursor.trim() === "") {
380 348 // Don't autocomplete if the part of the line before the cursor
381 349 // is empty. In this case, let CodeMirror handle indentation.
382 350 return false;
383 351 } else {
384 352 event.codemirrorIgnore = true;
385 353 event.preventDefault();
386 354 this.completer.startCompletion();
387 355 return true;
388 356 }
389 357 }
390 358
391 359 // keyboard event wasn't one of those unique to code cells, let's see
392 360 // if it's one of the generic ones (i.e. check edit mode shortcuts)
393 361 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
394 362 };
395 363
396 364 // Kernel related calls.
397 365
398 366 CodeCell.prototype.set_kernel = function (kernel) {
399 367 this.kernel = kernel;
400 368 };
401 369
402 370 /**
403 371 * Execute current code cell to the kernel
404 372 * @method execute
405 373 */
406 374 CodeCell.prototype.execute = function (stop_on_error) {
407 375 if (!this.kernel || !this.kernel.is_connected()) {
408 376 console.log("Can't execute, kernel is not connected.");
409 377 return;
410 378 }
411 379
412 this.active_output_area.clear_output(false, true);
380 this.output_area.clear_output(false, true);
413 381
414 382 if (stop_on_error === undefined) {
415 383 stop_on_error = true;
416 384 }
417 385
418 386 // Clear widget area
419 387 for (var i = 0; i < this.widget_views.length; i++) {
420 388 var view = this.widget_views[i];
421 389 view.remove();
422 390
423 391 // Remove widget live events.
424 392 view.off('comm:live', this._widget_live);
425 393 view.off('comm:dead', this._widget_dead);
426 394 }
427 395 this.widget_views = [];
428 396 this.widget_subarea.html('');
429 397 this.widget_subarea.height('');
430 398 this.widget_area.height('');
431 399 this.widget_area.hide();
432 400
433 401 this.set_input_prompt('*');
434 402 this.element.addClass("running");
435 403 if (this.last_msg_id) {
436 404 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
437 405 }
438 406 var callbacks = this.get_callbacks();
439 407
440 408 var old_msg_id = this.last_msg_id;
441 409 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true,
442 410 stop_on_error : stop_on_error});
443 411 if (old_msg_id) {
444 412 delete CodeCell.msg_cells[old_msg_id];
445 413 }
446 414 CodeCell.msg_cells[this.last_msg_id] = this;
447 415 this.render();
448 416 this.events.trigger('execute.CodeCell', {cell: this});
449 417 };
450 418
451 419 /**
452 420 * Construct the default callbacks for
453 421 * @method get_callbacks
454 422 */
455 423 CodeCell.prototype.get_callbacks = function () {
456 424 var that = this;
457 425 return {
458 426 shell : {
459 427 reply : $.proxy(this._handle_execute_reply, this),
460 428 payload : {
461 429 set_next_input : $.proxy(this._handle_set_next_input, this),
462 430 page : $.proxy(this._open_with_pager, this)
463 431 }
464 432 },
465 433 iopub : {
466 434 output : function() {
467 that.active_output_area.handle_output.apply(that.active_output_area, arguments);
435 that.output_area.handle_output.apply(that.output_area, arguments);
468 436 },
469 437 clear_output : function() {
470 that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments);
438 that.output_area.handle_clear_output.apply(that.output_area, arguments);
471 439 },
472 440 },
473 441 input : $.proxy(this._handle_input_request, this)
474 442 };
475 443 };
476 444
477 445 CodeCell.prototype._open_with_pager = function (payload) {
478 446 this.events.trigger('open_with_text.Pager', payload);
479 447 };
480 448
481 449 /**
482 450 * @method _handle_execute_reply
483 451 * @private
484 452 */
485 453 CodeCell.prototype._handle_execute_reply = function (msg) {
486 454 this.set_input_prompt(msg.content.execution_count);
487 455 this.element.removeClass("running");
488 456 this.events.trigger('set_dirty.Notebook', {value: true});
489 457 };
490 458
491 459 /**
492 460 * @method _handle_set_next_input
493 461 * @private
494 462 */
495 463 CodeCell.prototype._handle_set_next_input = function (payload) {
496 464 var data = {'cell': this, 'text': payload.text, replace: payload.replace};
497 465 this.events.trigger('set_next_input.Notebook', data);
498 466 };
499 467
500 468 /**
501 469 * @method _handle_input_request
502 470 * @private
503 471 */
504 472 CodeCell.prototype._handle_input_request = function (msg) {
505 this.active_output_area.append_raw_input(msg);
473 this.output_area.append_raw_input(msg);
506 474 };
507 475
508 476
509 477 // Basic cell manipulation.
510 478
511 479 CodeCell.prototype.select = function () {
512 480 var cont = Cell.prototype.select.apply(this);
513 481 if (cont) {
514 482 this.code_mirror.refresh();
515 483 this.auto_highlight();
516 484 }
517 485 return cont;
518 486 };
519 487
520 488 CodeCell.prototype.render = function () {
521 489 var cont = Cell.prototype.render.apply(this);
522 490 // Always execute, even if we are already in the rendered state
523 491 return cont;
524 492 };
525 493
526 494 CodeCell.prototype.select_all = function () {
527 495 var start = {line: 0, ch: 0};
528 496 var nlines = this.code_mirror.lineCount();
529 497 var last_line = this.code_mirror.getLine(nlines-1);
530 498 var end = {line: nlines-1, ch: last_line.length};
531 499 this.code_mirror.setSelection(start, end);
532 500 };
533 501
534 502
535 503 CodeCell.prototype.collapse_output = function () {
536 504 this.output_area.collapse();
537 505 };
538 506
539 507
540 508 CodeCell.prototype.expand_output = function () {
541 509 this.output_area.expand();
542 510 this.output_area.unscroll_area();
543 511 };
544 512
545 513 CodeCell.prototype.scroll_output = function () {
546 514 this.output_area.expand();
547 515 this.output_area.scroll_if_long();
548 516 };
549 517
550 518 CodeCell.prototype.toggle_output = function () {
551 519 this.output_area.toggle_output();
552 520 };
553 521
554 522 CodeCell.prototype.toggle_output_scroll = function () {
555 523 this.output_area.toggle_scroll();
556 524 };
557 525
558 526
559 527 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
560 528 var ns;
561 529 if (prompt_value === undefined || prompt_value === null) {
562 530 ns = "&nbsp;";
563 531 } else {
564 532 ns = encodeURIComponent(prompt_value);
565 533 }
566 534 return 'In&nbsp;[' + ns + ']:';
567 535 };
568 536
569 537 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
570 538 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
571 539 for(var i=1; i < lines_number; i++) {
572 540 html.push(['...:']);
573 541 }
574 542 return html.join('<br/>');
575 543 };
576 544
577 545 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
578 546
579 547
580 548 CodeCell.prototype.set_input_prompt = function (number) {
581 549 var nline = 1;
582 550 if (this.code_mirror !== undefined) {
583 551 nline = this.code_mirror.lineCount();
584 552 }
585 553 this.input_prompt_number = number;
586 554 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
587 555 // This HTML call is okay because the user contents are escaped.
588 556 this.element.find('div.input_prompt').html(prompt_html);
589 557 };
590 558
591 559
592 560 CodeCell.prototype.clear_input = function () {
593 561 this.code_mirror.setValue('');
594 562 };
595 563
596 564
597 565 CodeCell.prototype.get_text = function () {
598 566 return this.code_mirror.getValue();
599 567 };
600 568
601 569
602 570 CodeCell.prototype.set_text = function (code) {
603 571 return this.code_mirror.setValue(code);
604 572 };
605 573
606 574
607 575 CodeCell.prototype.clear_output = function (wait) {
608 this.active_output_area.clear_output(wait);
576 this.output_area.clear_output(wait);
609 577 this.set_input_prompt();
610 578 };
611 579
612 580
613 581 // JSON serialization
614 582
615 583 CodeCell.prototype.fromJSON = function (data) {
616 584 Cell.prototype.fromJSON.apply(this, arguments);
617 585 if (data.cell_type === 'code') {
618 586 if (data.source !== undefined) {
619 587 this.set_text(data.source);
620 588 // make this value the starting point, so that we can only undo
621 589 // to this state, instead of a blank cell
622 590 this.code_mirror.clearHistory();
623 591 this.auto_highlight();
624 592 }
625 593 this.set_input_prompt(data.execution_count);
626 594 this.output_area.trusted = data.metadata.trusted || false;
627 595 this.output_area.fromJSON(data.outputs);
628 596 if (data.metadata.collapsed !== undefined) {
629 597 if (data.metadata.collapsed) {
630 598 this.collapse_output();
631 599 } else {
632 600 this.expand_output();
633 601 }
634 602 }
635 603 }
636 604 };
637 605
638 606
639 607 CodeCell.prototype.toJSON = function () {
640 608 var data = Cell.prototype.toJSON.apply(this);
641 609 data.source = this.get_text();
642 610 // is finite protect against undefined and '*' value
643 611 if (isFinite(this.input_prompt_number)) {
644 612 data.execution_count = this.input_prompt_number;
645 613 } else {
646 614 data.execution_count = null;
647 615 }
648 616 var outputs = this.output_area.toJSON();
649 617 data.outputs = outputs;
650 618 data.metadata.trusted = this.output_area.trusted;
651 619 data.metadata.collapsed = this.output_area.collapsed;
652 620 return data;
653 621 };
654 622
655 623 /**
656 624 * handle cell level logic when a cell is unselected
657 625 * @method unselect
658 626 * @return is the action being taken
659 627 */
660 628 CodeCell.prototype.unselect = function () {
661 629 var cont = Cell.prototype.unselect.apply(this);
662 630 if (cont) {
663 631 // When a code cell is usnelected, make sure that the corresponding
664 632 // tooltip and completer to that cell is closed.
665 633 this.tooltip.remove_and_cancel_tooltip(true);
666 634 if (this.completer !== null) {
667 635 this.completer.close();
668 636 }
669 637 }
670 638 return cont;
671 639 };
672 640
673 641 // Backwards compatability.
674 642 IPython.CodeCell = CodeCell;
675 643
676 644 return {'CodeCell': CodeCell};
677 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,61 +1,64 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 _handle_route_msg: function(content) {
47 var cell = this.options.cell;
48 if (content && cell) {
49 if (content.method == 'push') {
50 cell.push_output_area(this.output_area);
51 } else if (content.method == 'pop') {
52 cell.pop_output_area(this.output_area);
46 /**
47 * Handles re-routed iopub messages.
48 */
49 _handle_route_msg: function(msg) {
50 if (msg) {
51 var msg_type = msg.msg_type;
52 if (msg_type=='clear_output') {
53 this.output_area.handle_clear_output(msg);
54 } else {
55 this.output_area.handle_output(msg);
53 56 }
54 57 }
55 58 },
56 59 });
57 60
58 61 return {
59 62 'OutputView': OutputView,
60 63 };
61 64 });
@@ -1,50 +1,78 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 from IPython.kernel.zmq.session import Message
14 15
15 16 @skip_doctest
16 17 class Output(DOMWidget):
17 18 """Widget used as a context manager to display output.
18 19
19 20 This widget can capture and display stdout, stderr, and rich output. To use
20 21 it, create an instance of it and display it. Then use it as a context
21 22 manager. Any output produced while in it's context will be captured and
22 23 displayed in it instead of the standard output area.
23 24
24 25 Example
25 26 from IPython.html import widgets
26 27 from IPython.display import display
27 28 out = widgets.Output()
28 29 display(out)
29 30
30 31 print('prints to output area')
31 32
32 33 with out:
33 34 print('prints to output widget')"""
34 35 _view_name = Unicode('OutputView', sync=True)
35 36
36 37 def clear_output(self, *pargs, **kwargs):
37 38 with self:
38 39 clear_output(*pargs, **kwargs)
39 40
40 41 def __enter__(self):
42 """Called upon entering output widget context manager."""
41 43 self._flush()
42 self.send({'method': 'push'})
44 kernel = get_ipython().kernel
45 session = kernel.session
46 send = session.send
47 self._original_send = send
48 self._session = session
49
50 def send_hook(stream, msg_or_type, content=None, parent=None, ident=None,
51 buffers=None, track=False, header=None, metadata=None):
52
53 # Handle both prebuild messages and unbuilt messages.
54 if isinstance(msg_or_type, (Message, dict)):
55 msg_type = msg_or_type['msg_type']
56 msg = dict(msg_or_type)
57 else:
58 msg_type = msg_or_type
59 msg = session.msg(msg_type, content=content, parent=parent,
60 header=header, metadata=metadata)
61
62 # If this is a message type that we want to forward, forward it.
63 if stream is kernel.iopub_socket and msg_type in ['clear_output', 'stream', 'display_data']:
64 self.send(msg)
65 else:
66 send(stream, msg, ident=ident, buffers=buffers, track=track)
67
68 session.send = send_hook
43 69
44 70 def __exit__(self, exception_type, exception_value, traceback):
71 """Called upon exiting output widget context manager."""
45 72 self._flush()
46 self.send({'method': 'pop'})
73 self._session.send = self._original_send
47 74
48 75 def _flush(self):
76 """Flush stdout and stderr buffers."""
49 77 sys.stdout.flush()
50 78 sys.stderr.flush()
General Comments 0
You need to be logged in to leave comments. Login now