// Copyright (c) IPython Development Team. // Distributed under the terms of the Modified BSD License. define([ 'base/js/namespace', 'jqueryui', 'base/js/utils', 'base/js/security', 'base/js/keyboard', 'notebook/js/mathjaxutils', 'components/marked/lib/marked', ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) { "use strict"; /** * @class OutputArea * * @constructor */ var OutputArea = function (options) { this.selector = options.selector; this.events = options.events; this.keyboard_manager = options.keyboard_manager; this.wrapper = $(options.selector); this.outputs = []; this.collapsed = false; this.scrolled = false; this.scroll_state = 'auto'; this.trusted = true; this.clear_queued = null; if (options.prompt_area === undefined) { this.prompt_area = true; } else { this.prompt_area = options.prompt_area; } this.create_elements(); this.style(); this.bind_events(); }; /** * Class prototypes **/ 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 btn-default output_collapsed"); this.collapse_button.attr('title', 'click to expand output'); this.collapse_button.text('. . .'); 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 the current threshold. * Threshold will be OutputArea.minimum_scroll_threshold if scroll_state=true (manually requested) * or OutputArea.auto_scroll_threshold if scroll_state='auto'. * This will always return false if scroll_state=false (scroll disabled). * */ OutputArea.prototype._should_scroll = function () { var threshold; if (this.scroll_state === false) { return false; } else if (this.scroll_state === true) { threshold = OutputArea.minimum_scroll_threshold; } else { threshold = OutputArea.auto_scroll_threshold; } if (threshold <=0) { return false; } // 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() > threshold * 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 ( 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()) { 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; // collapsing output clears scroll state this.scroll_state = 'auto'; } }; OutputArea.prototype.expand = function () { if (this.collapsed) { this.collapse_button.hide(); this.element.show(); if (this.prompt_area) { this.prompt_overlay.show(); } this.collapsed = false; this.scroll_if_long(); } }; 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; }; /** * Scroll OutputArea if height exceeds a threshold. * * Threshold is OutputArea.minimum_scroll_threshold if scroll_state = true, * OutputArea.auto_scroll_threshold if scroll_state='auto'. * **/ OutputArea.prototype.scroll_if_long = function () { var should_scroll = this._should_scroll(); if (!this.scrolled && should_scroll) { // only allow scrolling long-enough output this.scroll_area(); } else if (this.scrolled && !should_scroll) { // scrolled and shouldn't be this.unscroll_area(); } }; OutputArea.prototype.toggle_scroll = function () { if (this.scroll_state == 'auto') { this.scroll_state = !this.scrolled; } else { this.scroll_state = !this.scroll_state; } 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 () { utils.typeset(this.element); }; 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.text; json.name = content.name; } else if (msg_type === "display_data") { json.data = content.data; json.metadata = content.metadata; } else if (msg_type === "execute_result") { json.data = content.data; json.metadata = content.metadata; json.execution_count = content.execution_count; } else if (msg_type === "error") { json.ename = content.ename; json.evalue = content.evalue; json.traceback = content.traceback; } else { console.log("unhandled output message", msg); return; } this.append_output(json); }; OutputArea.output_types = [ 'application/javascript', 'text/html', 'text/markdown', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'application/pdf', 'text/plain' ]; OutputArea.prototype.validate_mimebundle = function (bundle) { /** scrub invalid outputs */ if (typeof bundle.data !== 'object') { console.warn("mimebundle missing data", bundle); bundle.data = {}; } if (typeof bundle.metadata !== 'object') { console.warn("mimebundle missing metadata", bundle); bundle.metadata = {}; } var data = bundle.data; $.map(OutputArea.output_types, function(key){ if (key !== 'application/json' && data[key] !== undefined && typeof data[key] !== 'string' ) { console.log("Invalid type for " + key, data[key]); delete data[key]; } }); return bundle; }; 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; } var record_output = true; switch(json.output_type) { case 'execute_result': json = this.validate_mimebundle(json); this.append_execute_result(json); break; case 'stream': // append_stream might have merged the output with earlier stream output record_output = this.append_stream(json); break; case 'error': this.append_error(json); break; case 'display_data': // append handled below json = this.validate_mimebundle(json); break; default: console.log("unrecognized output type: " + json.output_type); this.append_unrecognized(json); } // We must release the animation fixed height in a callback since Gecko // (FireFox) doesn't render the image immediately as the data is // available. var that = this; var handle_appended = function ($el) { /** * Only reset the height to automatic if the height is currently * fixed (done by wait=True flag on clear_output). */ if (needs_height_reset) { that.element.height(''); } that.element.trigger('resize'); }; if (json.output_type === 'display_data') { this.append_display_data(json, handle_appended); } else { handle_appended(); } if (record_output) { this.outputs.push(json); } }; 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. At this point, subarea doesn't // contain any user content. 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($('').text(msg).addClass('js-error')) .append($('').text(err.toString()).addClass('js-error')) .append($('').text('See your browser Javascript console for more details.').addClass('js-error')); }; OutputArea.prototype._safe_append = function (toinsert) { /** * safely append an item to the document * this is an object created by user code, * and may have errors, which should not be raised * under any circumstances. */ try { this.element.append(toinsert); } catch(err) { console.log(err); // Create an actual output_area and output_subarea, which creates // the prompt area and the proper indentation. var toinsert = this.create_output_area(); var subarea = $('').addClass('output_subarea'); toinsert.append(subarea); this._append_javascript_error(err, subarea); this.element.append(toinsert); } // Notify others of changes. this.element.trigger('changed'); }; OutputArea.prototype.append_execute_result = function (json) { var n = json.execution_count || ' '; var toinsert = this.create_output_area(); if (this.prompt_area) { toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:'); } var inserted = this.append_mime_type(json, toinsert); if (inserted) { inserted.addClass('output_result'); } this._safe_append(toinsert); // If we just output latex, typeset it. if ((json.data['text/latex'] !== undefined) || (json.data['text/html'] !== undefined) || (json.data['text/markdown'] !== undefined)) { this.typeset(); } }; OutputArea.prototype.append_error = function (json) { var tb = json.traceback; if (tb !== undefined && tb.length > 0) { var s = ''; var len = tb.length; for (var i=0; i