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