//---------------------------------------------------------------------------- // Copyright (C) 2008 The IPython Development Team // // Distributed under the terms of the BSD License. The full license is in // the file COPYING, distributed as part of this software. //---------------------------------------------------------------------------- //============================================================================ // OutputArea //============================================================================ /** * @module IPython * @namespace IPython * @submodule OutputArea */ var IPython = (function (IPython) { "use strict"; var utils = IPython.utils; /** * @class OutputArea * * @constructor */ var OutputArea = function (selector, prompt_area) { this.selector = selector; this.wrapper = $(selector); this.outputs = []; this.collapsed = false; this.scrolled = false; this.clear_queued = null; if (prompt_area === undefined) { this.prompt_area = true; } else { this.prompt_area = prompt_area; } this.create_elements(); this.style(); this.bind_events(); }; OutputArea.prototype.create_elements = function () { this.element = $("
"); this.collapse_button = $(""); this.prompt_overlay = $(""); this.wrapper.append(this.prompt_overlay); this.wrapper.append(this.element); this.wrapper.append(this.collapse_button); }; OutputArea.prototype.style = function () { this.collapse_button.hide(); this.prompt_overlay.hide(); this.wrapper.addClass('output_wrapper'); this.element.addClass('output'); this.collapse_button.addClass("btn output_collapsed"); this.collapse_button.attr('title', 'click to expand output'); this.collapse_button.html('. . .'); this.prompt_overlay.addClass('out_prompt_overlay prompt'); this.prompt_overlay.attr('title', 'click to expand output; double click to hide output'); this.collapse(); }; /** * Should the OutputArea scroll? * Returns whether the height (in lines) exceeds a threshold. * * @private * @method _should_scroll * @param [lines=100]{Integer} * @return {Bool} * */ OutputArea.prototype._should_scroll = function (lines) { if (lines <=0 ){ return } if (!lines) { lines = 100; } // line-height from http://stackoverflow.com/questions/1185151 var fontSize = this.element.css('font-size'); var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5); return (this.element.height() > lines * lineHeight); }; OutputArea.prototype.bind_events = function () { var that = this; this.prompt_overlay.dblclick(function () { that.toggle_output(); }); this.prompt_overlay.click(function () { that.toggle_scroll(); }); this.element.resize(function () { // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled if ( IPython.utils.browser[0] === "Firefox" ) { return; } // maybe scroll output, // if it's grown large enough and hasn't already been scrolled. if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) { that.scroll_area(); } }); this.collapse_button.click(function () { that.expand(); }); }; OutputArea.prototype.collapse = function () { if (!this.collapsed) { this.element.hide(); this.prompt_overlay.hide(); if (this.element.html()){ this.collapse_button.show(); } this.collapsed = true; } }; OutputArea.prototype.expand = function () { if (this.collapsed) { this.collapse_button.hide(); this.element.show(); this.prompt_overlay.show(); this.collapsed = false; } }; OutputArea.prototype.toggle_output = function () { if (this.collapsed) { this.expand(); } else { this.collapse(); } }; OutputArea.prototype.scroll_area = function () { this.element.addClass('output_scroll'); this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide'); this.scrolled = true; }; OutputArea.prototype.unscroll_area = function () { this.element.removeClass('output_scroll'); this.prompt_overlay.attr('title', 'click to scroll output; double click to hide'); this.scrolled = false; }; /** * Threshold to trigger autoscroll when the OutputArea is resized, * typically when new outputs are added. * * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold, * unless it is < 0, in which case autoscroll will never be triggered * * @property auto_scroll_threshold * @type Number * @default 100 * **/ OutputArea.auto_scroll_threshold = 100; /** * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas * shorter than this are never scrolled. * * @property minimum_scroll_threshold * @type Number * @default 20 * **/ OutputArea.minimum_scroll_threshold = 20; /** * * Scroll OutputArea if height supperior than a threshold (in lines). * * Threshold is a maximum number of lines. If unspecified, defaults to * OutputArea.minimum_scroll_threshold. * * Negative threshold will prevent the OutputArea from ever scrolling. * * @method scroll_if_long * * @param [lines=20]{Number} Default to 20 if not set, * behavior undefined for value of `0`. * **/ OutputArea.prototype.scroll_if_long = function (lines) { var n = lines | OutputArea.minimum_scroll_threshold; if(n <= 0){ return } if (this._should_scroll(n)) { // only allow scrolling long-enough output this.scroll_area(); } }; OutputArea.prototype.toggle_scroll = function () { if (this.scrolled) { this.unscroll_area(); } else { // only allow scrolling long-enough output this.scroll_if_long(); } }; // typeset with MathJax if MathJax is available OutputArea.prototype.typeset = function () { if (window.MathJax){ MathJax.Hub.Queue(["Typeset",MathJax.Hub]); } }; OutputArea.prototype.handle_output = function (msg) { var json = {}; var msg_type = json.output_type = msg.header.msg_type; var content = msg.content; if (msg_type === "stream") { json.text = content.data; json.stream = content.name; } else if (msg_type === "display_data") { json = content.data; json.output_type = msg_type; json.metadata = content.metadata; } else if (msg_type === "pyout") { json = content.data; json.output_type = msg_type; json.metadata = content.metadata; json.prompt_number = content.execution_count; } else if (msg_type === "pyerr") { json.ename = content.ename; json.evalue = content.evalue; json.traceback = content.traceback; } this.append_output(json); }; OutputArea.mime_map = { "text/plain" : "text", "text/html" : "html", "image/svg+xml" : "svg", "image/png" : "png", "image/jpeg" : "jpeg", "text/latex" : "latex", "application/json" : "json", "application/javascript" : "javascript", }; OutputArea.mime_map_r = { "text" : "text/plain", "html" : "text/html", "svg" : "image/svg+xml", "png" : "image/png", "jpeg" : "image/jpeg", "latex" : "text/latex", "json" : "application/json", "javascript" : "application/javascript", }; OutputArea.prototype.rename_keys = function (data, key_map) { var remapped = {}; for (var key in data) { var new_key = key_map[key] || key; remapped[new_key] = data[key]; } return remapped; }; OutputArea.output_types = [ 'application/javascript', 'text/html', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain' ]; OutputArea.prototype.validate_output = function (json) { // scrub invalid outputs // TODO: right now everything is a string, but JSON really shouldn't be. // nbformat 4 will fix that. $.map(OutputArea.output_types, function(key){ if (json[key] !== undefined && typeof json[key] !== 'string') { console.log("Invalid type for " + key, json[key]); delete json[key]; } }); return json; }; OutputArea.prototype.append_output = function (json) { this.expand(); // Clear the output if clear is queued. var needs_height_reset = false; if (this.clear_queued) { this.clear_output(false); needs_height_reset = true; } // validate output data types json = this.validate_output(json); if (json.output_type === 'pyout') { this.append_pyout(json); } else if (json.output_type === 'pyerr') { this.append_pyerr(json); } else if (json.output_type === 'display_data') { this.append_display_data(json); } else if (json.output_type === 'stream') { this.append_stream(json); } this.outputs.push(json); // Only reset the height to automatic if the height is currently // fixed (done by wait=True flag on clear_output). if (needs_height_reset) { this.element.height(''); } var that = this; setTimeout(function(){that.element.trigger('resize');}, 100); }; OutputArea.prototype.create_output_area = function () { var oa = $("").addClass("output_area"); if (this.prompt_area) { oa.append($('').addClass('prompt')); } return oa; }; function _get_metadata_key(metadata, key, mime) { var mime_md = metadata[mime]; // mime-specific higher priority if (mime_md && mime_md[key] !== undefined) { return mime_md[key]; } // fallback on global return metadata[key]; } OutputArea.prototype.create_output_subarea = function(md, classes, mime) { var subarea = $('').addClass('output_subarea').addClass(classes); if (_get_metadata_key(md, 'isolated', mime)) { // Create an iframe to isolate the subarea from the rest of the // document var iframe = $('').addClass('box-flex1'); iframe.css({'height':1, 'width':'100%', 'display':'block'}); iframe.attr('frameborder', 0); iframe.attr('scrolling', 'auto'); // Once the iframe is loaded, the subarea is dynamically inserted iframe.on('load', function() { // Workaround needed by Firefox, to properly render svg inside // iframes, see http://stackoverflow.com/questions/10177190/ // svg-dynamically-added-to-iframe-does-not-render-correctly this.contentDocument.open(); // Insert the subarea into the iframe // We must directly write the html. When using Jquery's append // method, javascript is evaluated in the parent document and // not in the iframe document. this.contentDocument.write(subarea.html()); this.contentDocument.close(); var body = this.contentDocument.body; // Adjust the iframe height automatically iframe.height(body.scrollHeight + 'px'); }); // Elements should be appended to the inner subarea and not to the // iframe iframe.append = function(that) { subarea.append(that); }; return iframe; } else { return subarea; } } OutputArea.prototype._append_javascript_error = function (err, element) { // display a message when a javascript error occurs in display output var msg = "Javascript error adding output!" if ( element === undefined ) return; element.append( $('').html(msg + "