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