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