##// END OF EJS Templates
Changed the display order of rich output in the live notebook to match that of nbconvert. This makes more sense than the other way around due to the need for notebooks already shared on the web to remain consistant. Conversely local notebooks can simply be edited to match the new order.
damontallen -
Show More
@@ -1,980 +1,980
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 if (window.MathJax){
203 203 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
204 204 }
205 205 };
206 206
207 207
208 208 OutputArea.prototype.handle_output = function (msg) {
209 209 var json = {};
210 210 var msg_type = json.output_type = msg.header.msg_type;
211 211 var content = msg.content;
212 212 if (msg_type === "stream") {
213 213 json.text = content.text;
214 214 json.name = content.name;
215 215 } else if (msg_type === "display_data") {
216 216 json.data = content.data;
217 217 json.output_type = msg_type;
218 218 json.metadata = content.metadata;
219 219 } else if (msg_type === "execute_result") {
220 220 json.data = content.data;
221 221 json.output_type = msg_type;
222 222 json.metadata = content.metadata;
223 223 json.execution_count = content.execution_count;
224 224 } else if (msg_type === "error") {
225 225 json.ename = content.ename;
226 226 json.evalue = content.evalue;
227 227 json.traceback = content.traceback;
228 228 } else {
229 229 console.log("unhandled output message", msg);
230 230 return;
231 231 }
232 232 this.append_output(json);
233 233 };
234 234
235 235
236 236 OutputArea.output_types = [
237 237 'application/javascript',
238 238 'text/html',
239 239 'text/markdown',
240 240 'text/latex',
241 241 'image/svg+xml',
242 242 'image/png',
243 243 'image/jpeg',
244 244 'application/pdf',
245 245 'text/plain'
246 246 ];
247 247
248 248 OutputArea.prototype.validate_mimebundle = function (json) {
249 249 /**
250 250 * scrub invalid outputs
251 251 */
252 252 var data = json.data;
253 253 $.map(OutputArea.output_types, function(key){
254 254 if (key !== 'application/json' &&
255 255 data[key] !== undefined &&
256 256 typeof data[key] !== 'string'
257 257 ) {
258 258 console.log("Invalid type for " + key, data[key]);
259 259 delete data[key];
260 260 }
261 261 });
262 262 return json;
263 263 };
264 264
265 265 OutputArea.prototype.append_output = function (json) {
266 266 this.expand();
267 267
268 268 // Clear the output if clear is queued.
269 269 var needs_height_reset = false;
270 270 if (this.clear_queued) {
271 271 this.clear_output(false);
272 272 needs_height_reset = true;
273 273 }
274 274
275 275 var record_output = true;
276 276 switch(json.output_type) {
277 277 case 'execute_result':
278 278 json = this.validate_mimebundle(json);
279 279 this.append_execute_result(json);
280 280 break;
281 281 case 'stream':
282 282 // append_stream might have merged the output with earlier stream output
283 283 record_output = this.append_stream(json);
284 284 break;
285 285 case 'error':
286 286 this.append_error(json);
287 287 break;
288 288 case 'display_data':
289 289 // append handled below
290 290 json = this.validate_mimebundle(json);
291 291 break;
292 292 default:
293 293 console.log("unrecognized output type: " + json.output_type);
294 294 this.append_unrecognized(json);
295 295 }
296 296
297 297 // We must release the animation fixed height in a callback since Gecko
298 298 // (FireFox) doesn't render the image immediately as the data is
299 299 // available.
300 300 var that = this;
301 301 var handle_appended = function ($el) {
302 302 /**
303 303 * Only reset the height to automatic if the height is currently
304 304 * fixed (done by wait=True flag on clear_output).
305 305 */
306 306 if (needs_height_reset) {
307 307 that.element.height('');
308 308 }
309 309 that.element.trigger('resize');
310 310 };
311 311 if (json.output_type === 'display_data') {
312 312 this.append_display_data(json, handle_appended);
313 313 } else {
314 314 handle_appended();
315 315 }
316 316
317 317 if (record_output) {
318 318 this.outputs.push(json);
319 319 }
320 320 };
321 321
322 322
323 323 OutputArea.prototype.create_output_area = function () {
324 324 var oa = $("<div/>").addClass("output_area");
325 325 if (this.prompt_area) {
326 326 oa.append($('<div/>').addClass('prompt'));
327 327 }
328 328 return oa;
329 329 };
330 330
331 331
332 332 function _get_metadata_key(metadata, key, mime) {
333 333 var mime_md = metadata[mime];
334 334 // mime-specific higher priority
335 335 if (mime_md && mime_md[key] !== undefined) {
336 336 return mime_md[key];
337 337 }
338 338 // fallback on global
339 339 return metadata[key];
340 340 }
341 341
342 342 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
343 343 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
344 344 if (_get_metadata_key(md, 'isolated', mime)) {
345 345 // Create an iframe to isolate the subarea from the rest of the
346 346 // document
347 347 var iframe = $('<iframe/>').addClass('box-flex1');
348 348 iframe.css({'height':1, 'width':'100%', 'display':'block'});
349 349 iframe.attr('frameborder', 0);
350 350 iframe.attr('scrolling', 'auto');
351 351
352 352 // Once the iframe is loaded, the subarea is dynamically inserted
353 353 iframe.on('load', function() {
354 354 // Workaround needed by Firefox, to properly render svg inside
355 355 // iframes, see http://stackoverflow.com/questions/10177190/
356 356 // svg-dynamically-added-to-iframe-does-not-render-correctly
357 357 this.contentDocument.open();
358 358
359 359 // Insert the subarea into the iframe
360 360 // We must directly write the html. When using Jquery's append
361 361 // method, javascript is evaluated in the parent document and
362 362 // not in the iframe document. At this point, subarea doesn't
363 363 // contain any user content.
364 364 this.contentDocument.write(subarea.html());
365 365
366 366 this.contentDocument.close();
367 367
368 368 var body = this.contentDocument.body;
369 369 // Adjust the iframe height automatically
370 370 iframe.height(body.scrollHeight + 'px');
371 371 });
372 372
373 373 // Elements should be appended to the inner subarea and not to the
374 374 // iframe
375 375 iframe.append = function(that) {
376 376 subarea.append(that);
377 377 };
378 378
379 379 return iframe;
380 380 } else {
381 381 return subarea;
382 382 }
383 383 };
384 384
385 385
386 386 OutputArea.prototype._append_javascript_error = function (err, element) {
387 387 /**
388 388 * display a message when a javascript error occurs in display output
389 389 */
390 390 var msg = "Javascript error adding output!";
391 391 if ( element === undefined ) return;
392 392 element
393 393 .append($('<div/>').text(msg).addClass('js-error'))
394 394 .append($('<div/>').text(err.toString()).addClass('js-error'))
395 395 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
396 396 };
397 397
398 398 OutputArea.prototype._safe_append = function (toinsert) {
399 399 /**
400 400 * safely append an item to the document
401 401 * this is an object created by user code,
402 402 * and may have errors, which should not be raised
403 403 * under any circumstances.
404 404 */
405 405 try {
406 406 this.element.append(toinsert);
407 407 } catch(err) {
408 408 console.log(err);
409 409 // Create an actual output_area and output_subarea, which creates
410 410 // the prompt area and the proper indentation.
411 411 var toinsert = this.create_output_area();
412 412 var subarea = $('<div/>').addClass('output_subarea');
413 413 toinsert.append(subarea);
414 414 this._append_javascript_error(err, subarea);
415 415 this.element.append(toinsert);
416 416 }
417 417
418 418 // Notify others of changes.
419 419 this.element.trigger('changed');
420 420 };
421 421
422 422
423 423 OutputArea.prototype.append_execute_result = function (json) {
424 424 var n = json.execution_count || ' ';
425 425 var toinsert = this.create_output_area();
426 426 if (this.prompt_area) {
427 427 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
428 428 }
429 429 var inserted = this.append_mime_type(json, toinsert);
430 430 if (inserted) {
431 431 inserted.addClass('output_result');
432 432 }
433 433 this._safe_append(toinsert);
434 434 // If we just output latex, typeset it.
435 435 if ((json.data['text/latex'] !== undefined) ||
436 436 (json.data['text/html'] !== undefined) ||
437 437 (json.data['text/markdown'] !== undefined)) {
438 438 this.typeset();
439 439 }
440 440 };
441 441
442 442
443 443 OutputArea.prototype.append_error = function (json) {
444 444 var tb = json.traceback;
445 445 if (tb !== undefined && tb.length > 0) {
446 446 var s = '';
447 447 var len = tb.length;
448 448 for (var i=0; i<len; i++) {
449 449 s = s + tb[i] + '\n';
450 450 }
451 451 s = s + '\n';
452 452 var toinsert = this.create_output_area();
453 453 var append_text = OutputArea.append_map['text/plain'];
454 454 if (append_text) {
455 455 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
456 456 }
457 457 this._safe_append(toinsert);
458 458 }
459 459 };
460 460
461 461
462 462 OutputArea.prototype.append_stream = function (json) {
463 463 var text = json.text;
464 464 var subclass = "output_"+json.name;
465 465 if (this.outputs.length > 0){
466 466 // have at least one output to consider
467 467 var last = this.outputs[this.outputs.length-1];
468 468 if (last.output_type == 'stream' && json.name == last.name){
469 469 // latest output was in the same stream,
470 470 // so append directly into its pre tag
471 471 // escape ANSI & HTML specials:
472 472 last.text = utils.fixCarriageReturn(last.text + json.text);
473 473 var pre = this.element.find('div.'+subclass).last().find('pre');
474 474 var html = utils.fixConsole(last.text);
475 475 // The only user content injected with this HTML call is
476 476 // escaped by the fixConsole() method.
477 477 pre.html(html);
478 478 // return false signals that we merged this output with the previous one,
479 479 // and the new output shouldn't be recorded.
480 480 return false;
481 481 }
482 482 }
483 483
484 484 if (!text.replace("\r", "")) {
485 485 // text is nothing (empty string, \r, etc.)
486 486 // so don't append any elements, which might add undesirable space
487 487 // return true to indicate the output should be recorded.
488 488 return true;
489 489 }
490 490
491 491 // If we got here, attach a new div
492 492 var toinsert = this.create_output_area();
493 493 var append_text = OutputArea.append_map['text/plain'];
494 494 if (append_text) {
495 495 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
496 496 }
497 497 this._safe_append(toinsert);
498 498 return true;
499 499 };
500 500
501 501
502 502 OutputArea.prototype.append_unrecognized = function (json) {
503 503 var that = this;
504 504 var toinsert = this.create_output_area();
505 505 var subarea = $('<div/>').addClass('output_subarea output_unrecognized');
506 506 toinsert.append(subarea);
507 507 subarea.append(
508 508 $("<a>")
509 509 .attr("href", "#")
510 510 .text("Unrecognized output: " + json.output_type)
511 511 .click(function () {
512 512 that.events.trigger('unrecognized_output.OutputArea', {output: json})
513 513 })
514 514 );
515 515 this._safe_append(toinsert);
516 516 };
517 517
518 518
519 519 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
520 520 var toinsert = this.create_output_area();
521 521 if (this.append_mime_type(json, toinsert, handle_inserted)) {
522 522 this._safe_append(toinsert);
523 523 // If we just output latex, typeset it.
524 524 if ((json.data['text/latex'] !== undefined) ||
525 525 (json.data['text/html'] !== undefined) ||
526 526 (json.data['text/markdown'] !== undefined)) {
527 527 this.typeset();
528 528 }
529 529 }
530 530 };
531 531
532 532
533 533 OutputArea.safe_outputs = {
534 534 'text/plain' : true,
535 535 'text/latex' : true,
536 536 'image/png' : true,
537 537 'image/jpeg' : true
538 538 };
539 539
540 540 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
541 541 for (var i=0; i < OutputArea.display_order.length; i++) {
542 542 var type = OutputArea.display_order[i];
543 543 var append = OutputArea.append_map[type];
544 544 if ((json.data[type] !== undefined) && append) {
545 545 var value = json.data[type];
546 546 if (!this.trusted && !OutputArea.safe_outputs[type]) {
547 547 // not trusted, sanitize HTML
548 548 if (type==='text/html' || type==='text/svg') {
549 549 value = security.sanitize_html(value);
550 550 } else {
551 551 // don't display if we don't know how to sanitize it
552 552 console.log("Ignoring untrusted " + type + " output.");
553 553 continue;
554 554 }
555 555 }
556 556 var md = json.metadata || {};
557 557 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
558 558 // Since only the png and jpeg mime types call the inserted
559 559 // callback, if the mime type is something other we must call the
560 560 // inserted callback only when the element is actually inserted
561 561 // into the DOM. Use a timeout of 0 to do this.
562 562 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
563 563 setTimeout(handle_inserted, 0);
564 564 }
565 565 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
566 566 return toinsert;
567 567 }
568 568 }
569 569 return null;
570 570 };
571 571
572 572
573 573 var append_html = function (html, md, element) {
574 574 var type = 'text/html';
575 575 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
576 576 this.keyboard_manager.register_events(toinsert);
577 577 toinsert.append(html);
578 578 element.append(toinsert);
579 579 return toinsert;
580 580 };
581 581
582 582
583 583 var append_markdown = function(markdown, md, element) {
584 584 var type = 'text/markdown';
585 585 var toinsert = this.create_output_subarea(md, "output_markdown", type);
586 586 var text_and_math = mathjaxutils.remove_math(markdown);
587 587 var text = text_and_math[0];
588 588 var math = text_and_math[1];
589 589 marked(text, function (err, html) {
590 590 html = mathjaxutils.replace_math(html, math);
591 591 toinsert.append(html);
592 592 });
593 593 element.append(toinsert);
594 594 return toinsert;
595 595 };
596 596
597 597
598 598 var append_javascript = function (js, md, element) {
599 599 /**
600 600 * We just eval the JS code, element appears in the local scope.
601 601 */
602 602 var type = 'application/javascript';
603 603 var toinsert = this.create_output_subarea(md, "output_javascript", type);
604 604 this.keyboard_manager.register_events(toinsert);
605 605 element.append(toinsert);
606 606
607 607 // Fix for ipython/issues/5293, make sure `element` is the area which
608 608 // output can be inserted into at the time of JS execution.
609 609 element = toinsert;
610 610 try {
611 611 eval(js);
612 612 } catch(err) {
613 613 console.log(err);
614 614 this._append_javascript_error(err, toinsert);
615 615 }
616 616 return toinsert;
617 617 };
618 618
619 619
620 620 var append_text = function (data, md, element) {
621 621 var type = 'text/plain';
622 622 var toinsert = this.create_output_subarea(md, "output_text", type);
623 623 // escape ANSI & HTML specials in plaintext:
624 624 data = utils.fixConsole(data);
625 625 data = utils.fixCarriageReturn(data);
626 626 data = utils.autoLinkUrls(data);
627 627 // The only user content injected with this HTML call is
628 628 // escaped by the fixConsole() method.
629 629 toinsert.append($("<pre/>").html(data));
630 630 element.append(toinsert);
631 631 return toinsert;
632 632 };
633 633
634 634
635 635 var append_svg = function (svg_html, md, element) {
636 636 var type = 'image/svg+xml';
637 637 var toinsert = this.create_output_subarea(md, "output_svg", type);
638 638
639 639 // Get the svg element from within the HTML.
640 640 var svg = $('<div />').html(svg_html).find('svg');
641 641 var svg_area = $('<div />');
642 642 var width = svg.attr('width');
643 643 var height = svg.attr('height');
644 644 svg
645 645 .width('100%')
646 646 .height('100%');
647 647 svg_area
648 648 .width(width)
649 649 .height(height);
650 650
651 651 // The jQuery resize handlers don't seem to work on the svg element.
652 652 // When the svg renders completely, measure it's size and set the parent
653 653 // div to that size. Then set the svg to 100% the size of the parent
654 654 // div and make the parent div resizable.
655 655 this._dblclick_to_reset_size(svg_area, true, false);
656 656
657 657 svg_area.append(svg);
658 658 toinsert.append(svg_area);
659 659 element.append(toinsert);
660 660
661 661 return toinsert;
662 662 };
663 663
664 664 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
665 665 /**
666 666 * Add a resize handler to an element
667 667 *
668 668 * img: jQuery element
669 669 * immediately: bool=False
670 670 * Wait for the element to load before creating the handle.
671 671 * resize_parent: bool=True
672 672 * Should the parent of the element be resized when the element is
673 673 * reset (by double click).
674 674 */
675 675 var callback = function (){
676 676 var h0 = img.height();
677 677 var w0 = img.width();
678 678 if (!(h0 && w0)) {
679 679 // zero size, don't make it resizable
680 680 return;
681 681 }
682 682 img.resizable({
683 683 aspectRatio: true,
684 684 autoHide: true
685 685 });
686 686 img.dblclick(function () {
687 687 // resize wrapper & image together for some reason:
688 688 img.height(h0);
689 689 img.width(w0);
690 690 if (resize_parent === undefined || resize_parent) {
691 691 img.parent().height(h0);
692 692 img.parent().width(w0);
693 693 }
694 694 });
695 695 };
696 696
697 697 if (immediately) {
698 698 callback();
699 699 } else {
700 700 img.on("load", callback);
701 701 }
702 702 };
703 703
704 704 var set_width_height = function (img, md, mime) {
705 705 /**
706 706 * set width and height of an img element from metadata
707 707 */
708 708 var height = _get_metadata_key(md, 'height', mime);
709 709 if (height !== undefined) img.attr('height', height);
710 710 var width = _get_metadata_key(md, 'width', mime);
711 711 if (width !== undefined) img.attr('width', width);
712 712 };
713 713
714 714 var append_png = function (png, md, element, handle_inserted) {
715 715 var type = 'image/png';
716 716 var toinsert = this.create_output_subarea(md, "output_png", type);
717 717 var img = $("<img/>");
718 718 if (handle_inserted !== undefined) {
719 719 img.on('load', function(){
720 720 handle_inserted(img);
721 721 });
722 722 }
723 723 img[0].src = 'data:image/png;base64,'+ png;
724 724 set_width_height(img, md, 'image/png');
725 725 this._dblclick_to_reset_size(img);
726 726 toinsert.append(img);
727 727 element.append(toinsert);
728 728 return toinsert;
729 729 };
730 730
731 731
732 732 var append_jpeg = function (jpeg, md, element, handle_inserted) {
733 733 var type = 'image/jpeg';
734 734 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
735 735 var img = $("<img/>");
736 736 if (handle_inserted !== undefined) {
737 737 img.on('load', function(){
738 738 handle_inserted(img);
739 739 });
740 740 }
741 741 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
742 742 set_width_height(img, md, 'image/jpeg');
743 743 this._dblclick_to_reset_size(img);
744 744 toinsert.append(img);
745 745 element.append(toinsert);
746 746 return toinsert;
747 747 };
748 748
749 749
750 750 var append_pdf = function (pdf, md, element) {
751 751 var type = 'application/pdf';
752 752 var toinsert = this.create_output_subarea(md, "output_pdf", type);
753 753 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
754 754 a.attr('target', '_blank');
755 755 a.text('View PDF');
756 756 toinsert.append(a);
757 757 element.append(toinsert);
758 758 return toinsert;
759 759 };
760 760
761 761 var append_latex = function (latex, md, element) {
762 762 /**
763 763 * This method cannot do the typesetting because the latex first has to
764 764 * be on the page.
765 765 */
766 766 var type = 'text/latex';
767 767 var toinsert = this.create_output_subarea(md, "output_latex", type);
768 768 toinsert.append(latex);
769 769 element.append(toinsert);
770 770 return toinsert;
771 771 };
772 772
773 773
774 774 OutputArea.prototype.append_raw_input = function (msg) {
775 775 var that = this;
776 776 this.expand();
777 777 var content = msg.content;
778 778 var area = this.create_output_area();
779 779
780 780 // disable any other raw_inputs, if they are left around
781 781 $("div.output_subarea.raw_input_container").remove();
782 782
783 783 var input_type = content.password ? 'password' : 'text';
784 784
785 785 area.append(
786 786 $("<div/>")
787 787 .addClass("box-flex1 output_subarea raw_input_container")
788 788 .append(
789 789 $("<span/>")
790 790 .addClass("raw_input_prompt")
791 791 .text(content.prompt)
792 792 )
793 793 .append(
794 794 $("<input/>")
795 795 .addClass("raw_input")
796 796 .attr('type', input_type)
797 797 .attr("size", 47)
798 798 .keydown(function (event, ui) {
799 799 // make sure we submit on enter,
800 800 // and don't re-execute the *cell* on shift-enter
801 801 if (event.which === keyboard.keycodes.enter) {
802 802 that._submit_raw_input();
803 803 return false;
804 804 }
805 805 })
806 806 )
807 807 );
808 808
809 809 this.element.append(area);
810 810 var raw_input = area.find('input.raw_input');
811 811 // Register events that enable/disable the keyboard manager while raw
812 812 // input is focused.
813 813 this.keyboard_manager.register_events(raw_input);
814 814 // Note, the following line used to read raw_input.focus().focus().
815 815 // This seemed to be needed otherwise only the cell would be focused.
816 816 // But with the modal UI, this seems to work fine with one call to focus().
817 817 raw_input.focus();
818 818 };
819 819
820 820 OutputArea.prototype._submit_raw_input = function (evt) {
821 821 var container = this.element.find("div.raw_input_container");
822 822 var theprompt = container.find("span.raw_input_prompt");
823 823 var theinput = container.find("input.raw_input");
824 824 var value = theinput.val();
825 825 var echo = value;
826 826 // don't echo if it's a password
827 827 if (theinput.attr('type') == 'password') {
828 828 echo = 'Β·Β·Β·Β·Β·Β·Β·Β·';
829 829 }
830 830 var content = {
831 831 output_type : 'stream',
832 832 name : 'stdout',
833 833 text : theprompt.text() + echo + '\n'
834 834 };
835 835 // remove form container
836 836 container.parent().remove();
837 837 // replace with plaintext version in stdout
838 838 this.append_output(content, false);
839 839 this.events.trigger('send_input_reply.Kernel', value);
840 840 };
841 841
842 842
843 843 OutputArea.prototype.handle_clear_output = function (msg) {
844 844 /**
845 845 * msg spec v4 had stdout, stderr, display keys
846 846 * v4.1 replaced these with just wait
847 847 * The default behavior is the same (stdout=stderr=display=True, wait=False),
848 848 * so v4 messages will still be properly handled,
849 849 * except for the rarely used clearing less than all output.
850 850 */
851 851 this.clear_output(msg.content.wait || false);
852 852 };
853 853
854 854
855 855 OutputArea.prototype.clear_output = function(wait) {
856 856 if (wait) {
857 857
858 858 // If a clear is queued, clear before adding another to the queue.
859 859 if (this.clear_queued) {
860 860 this.clear_output(false);
861 861 }
862 862
863 863 this.clear_queued = true;
864 864 } else {
865 865
866 866 // Fix the output div's height if the clear_output is waiting for
867 867 // new output (it is being used in an animation).
868 868 if (this.clear_queued) {
869 869 var height = this.element.height();
870 870 this.element.height(height);
871 871 this.clear_queued = false;
872 872 }
873 873
874 874 // Clear all
875 875 // Remove load event handlers from img tags because we don't want
876 876 // them to fire if the image is never added to the page.
877 877 this.element.find('img').off('load');
878 878 this.element.html("");
879 879
880 880 // Notify others of changes.
881 881 this.element.trigger('changed');
882 882
883 883 this.outputs = [];
884 884 this.trusted = true;
885 885 this.unscroll_area();
886 886 return;
887 887 }
888 888 };
889 889
890 890
891 891 // JSON serialization
892 892
893 893 OutputArea.prototype.fromJSON = function (outputs, metadata) {
894 894 var len = outputs.length;
895 895 metadata = metadata || {};
896 896
897 897 for (var i=0; i<len; i++) {
898 898 this.append_output(outputs[i]);
899 899 }
900 900
901 901 if (metadata.collapsed !== undefined) {
902 902 this.collapsed = metadata.collapsed;
903 903 if (metadata.collapsed) {
904 904 this.collapse_output();
905 905 }
906 906 }
907 907 if (metadata.autoscroll !== undefined) {
908 908 this.collapsed = metadata.collapsed;
909 909 if (metadata.collapsed) {
910 910 this.collapse_output();
911 911 } else {
912 912 this.expand_output();
913 913 }
914 914 }
915 915 };
916 916
917 917
918 918 OutputArea.prototype.toJSON = function () {
919 919 return this.outputs;
920 920 };
921 921
922 922 /**
923 923 * Class properties
924 924 **/
925 925
926 926 /**
927 927 * Threshold to trigger autoscroll when the OutputArea is resized,
928 928 * typically when new outputs are added.
929 929 *
930 930 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
931 931 * unless it is < 0, in which case autoscroll will never be triggered
932 932 *
933 933 * @property auto_scroll_threshold
934 934 * @type Number
935 935 * @default 100
936 936 *
937 937 **/
938 938 OutputArea.auto_scroll_threshold = 100;
939 939
940 940 /**
941 941 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
942 942 * shorter than this are never scrolled.
943 943 *
944 944 * @property minimum_scroll_threshold
945 945 * @type Number
946 946 * @default 20
947 947 *
948 948 **/
949 949 OutputArea.minimum_scroll_threshold = 20;
950 950
951 951
952 952 OutputArea.display_order = [
953 953 'application/javascript',
954 954 'text/html',
955 955 'text/markdown',
956 'text/latex',
957 956 'image/svg+xml',
957 'text/latex',
958 958 'image/png',
959 959 'image/jpeg',
960 960 'application/pdf',
961 961 'text/plain'
962 962 ];
963 963
964 964 OutputArea.append_map = {
965 965 "text/plain" : append_text,
966 966 "text/html" : append_html,
967 967 "text/markdown": append_markdown,
968 968 "image/svg+xml" : append_svg,
969 969 "image/png" : append_png,
970 970 "image/jpeg" : append_jpeg,
971 971 "text/latex" : append_latex,
972 972 "application/javascript" : append_javascript,
973 973 "application/pdf" : append_pdf
974 974 };
975 975
976 976 // For backwards compatability.
977 977 IPython.OutputArea = OutputArea;
978 978
979 979 return {'OutputArea': OutputArea};
980 980 });
General Comments 0
You need to be logged in to leave comments. Login now