outputarea.js
969 lines
| 32.4 KiB
| application/javascript
|
JavascriptLexer
|
r16568 | // Copyright (c) IPython Development Team. | ||
// Distributed under the terms of the Modified BSD License. | ||||
|
r7177 | |||
|
r17198 | define([ | ||
'base/js/namespace', | ||||
|
r17215 | 'jqueryui', | ||
|
r17198 | 'base/js/utils', | ||
'base/js/security', | ||||
'base/js/keyboard', | ||||
'notebook/js/mathjaxutils', | ||||
|
r17389 | 'components/marked/lib/marked', | ||
], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) { | ||||
|
r7177 | "use strict"; | ||
|
r10776 | /** | ||
* @class OutputArea | ||||
* | ||||
* @constructor | ||||
*/ | ||||
|
r17214 | var OutputArea = function (options) { | ||
this.selector = options.selector; | ||||
this.events = options.events; | ||||
this.keyboard_manager = options.keyboard_manager; | ||||
this.wrapper = $(options.selector); | ||||
|
r7177 | this.outputs = []; | ||
this.collapsed = false; | ||||
|
r7362 | this.scrolled = false; | ||
|
r20058 | this.scroll_state = 'auto'; | ||
|
r14856 | this.trusted = true; | ||
|
r12592 | this.clear_queued = null; | ||
|
r17214 | if (options.prompt_area === undefined) { | ||
|
r7177 | this.prompt_area = true; | ||
|
r7181 | } else { | ||
|
r17214 | this.prompt_area = options.prompt_area; | ||
|
r9555 | } | ||
|
r7362 | this.create_elements(); | ||
|
r7177 | this.style(); | ||
|
r7362 | this.bind_events(); | ||
}; | ||||
|
r14886 | |||
/** | ||||
* Class prototypes | ||||
**/ | ||||
|
r7362 | OutputArea.prototype.create_elements = function () { | ||
this.element = $("<div/>"); | ||||
this.collapse_button = $("<div/>"); | ||||
this.prompt_overlay = $("<div/>"); | ||||
this.wrapper.append(this.prompt_overlay); | ||||
this.wrapper.append(this.element); | ||||
this.wrapper.append(this.collapse_button); | ||||
|
r7177 | }; | ||
OutputArea.prototype.style = function () { | ||||
|
r7362 | this.collapse_button.hide(); | ||
this.prompt_overlay.hide(); | ||||
this.wrapper.addClass('output_wrapper'); | ||||
|
r13332 | this.element.addClass('output'); | ||
|
r7362 | |||
|
r16913 | this.collapse_button.addClass("btn btn-default output_collapsed"); | ||
|
r9903 | this.collapse_button.attr('title', 'click to expand output'); | ||
|
r14634 | this.collapse_button.text('. . .'); | ||
|
r7362 | |||
this.prompt_overlay.addClass('out_prompt_overlay prompt'); | ||||
|
r9903 | this.prompt_overlay.attr('title', 'click to expand output; double click to hide output'); | ||
|
r7362 | |||
|
r7177 | this.collapse(); | ||
}; | ||||
|
r10776 | /** | ||
* Should the OutputArea scroll? | ||||
|
r20058 | * 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). | ||||
|
r10776 | * | ||
*/ | ||||
|
r20058 | 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; | ||||
|
r7362 | } | ||
// 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); | ||||
|
r20058 | return (this.element.height() > threshold * lineHeight); | ||
|
r7362 | }; | ||
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 () { | ||||
|
r7733 | // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled | ||
|
r17198 | if ( utils.browser[0] === "Firefox" ) { | ||
|
r7733 | return; | ||
} | ||||
|
r7362 | // maybe scroll output, | ||
// if it's grown large enough and hasn't already been scrolled. | ||||
|
r20058 | if (!that.scrolled && that._should_scroll()) { | ||
|
r7362 | that.scroll_area(); | ||
} | ||||
}); | ||||
this.collapse_button.click(function () { | ||||
that.expand(); | ||||
}); | ||||
}; | ||||
|
r7177 | OutputArea.prototype.collapse = function () { | ||
if (!this.collapsed) { | ||||
this.element.hide(); | ||||
|
r7362 | this.prompt_overlay.hide(); | ||
if (this.element.html()){ | ||||
this.collapse_button.show(); | ||||
} | ||||
|
r7177 | this.collapsed = true; | ||
|
r20058 | // collapsing output clears scroll state | ||
this.scroll_state = 'auto'; | ||||
|
r9555 | } | ||
|
r7177 | }; | ||
OutputArea.prototype.expand = function () { | ||||
if (this.collapsed) { | ||||
|
r7362 | this.collapse_button.hide(); | ||
|
r7177 | this.element.show(); | ||
|
r20299 | if (this.prompt_area) { | ||
this.prompt_overlay.show(); | ||||
} | ||||
|
r7177 | this.collapsed = false; | ||
|
r20058 | this.scroll_if_long(); | ||
|
r9555 | } | ||
|
r7177 | }; | ||
OutputArea.prototype.toggle_output = function () { | ||||
if (this.collapsed) { | ||||
this.expand(); | ||||
} else { | ||||
this.collapse(); | ||||
|
r9555 | } | ||
|
r7177 | }; | ||
|
r7362 | OutputArea.prototype.scroll_area = function () { | ||
this.element.addClass('output_scroll'); | ||||
|
r7423 | this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide'); | ||
|
r7362 | this.scrolled = true; | ||
}; | ||||
OutputArea.prototype.unscroll_area = function () { | ||||
this.element.removeClass('output_scroll'); | ||||
|
r7423 | this.prompt_overlay.attr('title', 'click to scroll output; double click to hide'); | ||
|
r7362 | this.scrolled = false; | ||
}; | ||||
|
r10776 | /** | ||
|
r20058 | * Scroll OutputArea if height exceeds a threshold. | ||
|
r10776 | * | ||
|
r20058 | * Threshold is OutputArea.minimum_scroll_threshold if scroll_state = true, | ||
* OutputArea.auto_scroll_threshold if scroll_state='auto'. | ||||
|
r10776 | * | ||
**/ | ||||
|
r20058 | OutputArea.prototype.scroll_if_long = function () { | ||
var should_scroll = this._should_scroll(); | ||||
if (!this.scrolled && should_scroll) { | ||||
|
r7362 | // only allow scrolling long-enough output | ||
this.scroll_area(); | ||||
|
r20058 | } else if (this.scrolled && !should_scroll) { | ||
// scrolled and shouldn't be | ||||
this.unscroll_area(); | ||||
|
r9555 | } | ||
|
r7362 | }; | ||
OutputArea.prototype.toggle_scroll = function () { | ||||
|
r20058 | if (this.scroll_state == 'auto') { | ||
|
r20059 | this.scroll_state = !this.scrolled; | ||
|
r20058 | } else { | ||
this.scroll_state = !this.scroll_state; | ||||
} | ||||
|
r7362 | if (this.scrolled) { | ||
this.unscroll_area(); | ||||
} else { | ||||
// only allow scrolling long-enough output | ||||
|
r10776 | this.scroll_if_long(); | ||
|
r9555 | } | ||
|
r7362 | }; | ||
|
r7177 | // typeset with MathJax if MathJax is available | ||
OutputArea.prototype.typeset = function () { | ||||
|
r19235 | utils.typeset(this.element); | ||
|
r7177 | }; | ||
|
r13207 | OutputArea.prototype.handle_output = function (msg) { | ||
|
r7177 | var json = {}; | ||
|
r13207 | var msg_type = json.output_type = msg.header.msg_type; | ||
var content = msg.content; | ||||
|
r7177 | if (msg_type === "stream") { | ||
|
r18104 | json.text = content.text; | ||
|
r18584 | json.name = content.name; | ||
|
r7177 | } else if (msg_type === "display_data") { | ||
|
r18592 | json.data = content.data; | ||
|
r14131 | json.metadata = content.metadata; | ||
|
r16568 | } else if (msg_type === "execute_result") { | ||
|
r18592 | json.data = content.data; | ||
|
r14131 | json.metadata = content.metadata; | ||
|
r18587 | json.execution_count = content.execution_count; | ||
|
r16569 | } else if (msg_type === "error") { | ||
|
r16589 | json.ename = content.ename; | ||
json.evalue = content.evalue; | ||||
json.traceback = content.traceback; | ||||
} else { | ||||
console.log("unhandled output message", msg); | ||||
return; | ||||
|
r9555 | } | ||
|
r14134 | this.append_output(json); | ||
|
r7177 | }; | ||
|
r14118 | |||
|
r14157 | |||
|
r14158 | OutputArea.output_types = [ | ||
'application/javascript', | ||||
'text/html', | ||||
|
r16364 | 'text/markdown', | ||
|
r14158 | 'text/latex', | ||
'image/svg+xml', | ||||
'image/png', | ||||
'image/jpeg', | ||||
|
r15127 | 'application/pdf', | ||
|
r14158 | 'text/plain' | ||
]; | ||||
|
r19803 | 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; | ||||
|
r14158 | $.map(OutputArea.output_types, function(key){ | ||
|
r18584 | if (key !== 'application/json' && | ||
|
r18592 | data[key] !== undefined && | ||
typeof data[key] !== 'string' | ||||
|
r18584 | ) { | ||
|
r18592 | console.log("Invalid type for " + key, data[key]); | ||
delete data[key]; | ||||
|
r14158 | } | ||
}); | ||||
|
r19803 | return bundle; | ||
|
r14158 | }; | ||
|
r14856 | |||
|
r14134 | OutputArea.prototype.append_output = function (json) { | ||
|
r7177 | this.expand(); | ||
|
r16191 | |||
|
r12592 | // Clear the output if clear is queued. | ||
|
r12817 | var needs_height_reset = false; | ||
|
r12592 | if (this.clear_queued) { | ||
this.clear_output(false); | ||||
|
r12817 | needs_height_reset = true; | ||
|
r12592 | } | ||
|
r16187 | |||
|
r17305 | var record_output = true; | ||
|
r18999 | 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); | ||||
|
r9555 | } | ||
|
r16187 | |||
|
r16191 | // 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) { | ||||
|
r19176 | /** | ||
* Only reset the height to automatic if the height is currently | ||||
* fixed (done by wait=True flag on clear_output). | ||||
*/ | ||||
|
r16191 | if (needs_height_reset) { | ||
that.element.height(''); | ||||
} | ||||
that.element.trigger('resize'); | ||||
}; | ||||
|
r16187 | if (json.output_type === 'display_data') { | ||
this.append_display_data(json, handle_appended); | ||||
|
r16191 | } else { | ||
|
r16187 | handle_appended(); | ||
} | ||||
|
r14856 | |||
|
r17305 | if (record_output) { | ||
this.outputs.push(json); | ||||
} | ||||
|
r7177 | }; | ||
OutputArea.prototype.create_output_area = function () { | ||||
|
r10216 | var oa = $("<div/>").addClass("output_area"); | ||
|
r7177 | if (this.prompt_area) { | ||
oa.append($('<div/>').addClass('prompt')); | ||||
} | ||||
return oa; | ||||
}; | ||||
|
r13412 | |||
|
r14116 | 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) { | ||||
|
r13412 | var subarea = $('<div/>').addClass('output_subarea').addClass(classes); | ||
|
r14116 | if (_get_metadata_key(md, 'isolated', mime)) { | ||
|
r13412 | // Create an iframe to isolate the subarea from the rest of the | ||
// document | ||||
|
r13418 | var iframe = $('<iframe/>').addClass('box-flex1'); | ||
|
r13419 | iframe.css({'height':1, 'width':'100%', 'display':'block'}); | ||
|
r13412 | 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 | ||||
|
r15376 | // not in the iframe document. At this point, subarea doesn't | ||
// contain any user content. | ||||
|
r13412 | this.contentDocument.write(subarea.html()); | ||
this.contentDocument.close(); | ||||
var body = this.contentDocument.body; | ||||
|
r13418 | // Adjust the iframe height automatically | ||
|
r13412 | 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; | ||||
} | ||||
|
r18591 | }; | ||
|
r13412 | |||
|
r13792 | OutputArea.prototype._append_javascript_error = function (err, element) { | ||
|
r19176 | /** | ||
* display a message when a javascript error occurs in display output | ||||
*/ | ||||
|
r18591 | var msg = "Javascript error adding output!"; | ||
|
r13792 | if ( element === undefined ) return; | ||
|
r15376 | element | ||
.append($('<div/>').text(msg).addClass('js-error')) | ||||
.append($('<div/>').text(err.toString()).addClass('js-error')) | ||||
.append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error')); | ||||
|
r12332 | }; | ||
OutputArea.prototype._safe_append = function (toinsert) { | ||||
|
r19176 | /** | ||
* 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. | ||||
*/ | ||||
|
r12332 | try { | ||
this.element.append(toinsert); | ||||
} catch(err) { | ||||
console.log(err); | ||||
|
r13792 | // 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 = $('<div/>').addClass('output_subarea'); | ||||
toinsert.append(subarea); | ||||
this._append_javascript_error(err, subarea); | ||||
this.element.append(toinsert); | ||||
|
r12332 | } | ||
|
r18954 | |||
// Notify others of changes. | ||||
this.element.trigger('changed'); | ||||
|
r12332 | }; | ||
|
r7177 | |||
|
r16568 | OutputArea.prototype.append_execute_result = function (json) { | ||
|
r18587 | var n = json.execution_count || ' '; | ||
|
r7177 | var toinsert = this.create_output_area(); | ||
if (this.prompt_area) { | ||||
|
r14634 | toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:'); | ||
|
r7177 | } | ||
|
r15729 | var inserted = this.append_mime_type(json, toinsert); | ||
if (inserted) { | ||||
|
r16589 | inserted.addClass('output_result'); | ||
|
r15729 | } | ||
|
r12332 | this._safe_append(toinsert); | ||
|
r7177 | // If we just output latex, typeset it. | ||
|
r18939 | if ((json.data['text/latex'] !== undefined) || | ||
(json.data['text/html'] !== undefined) || | ||||
(json.data['text/markdown'] !== undefined)) { | ||||
|
r7177 | this.typeset(); | ||
|
r9555 | } | ||
|
r7177 | }; | ||
|
r16569 | OutputArea.prototype.append_error = function (json) { | ||
|
r7177 | var tb = json.traceback; | ||
if (tb !== undefined && tb.length > 0) { | ||||
var s = ''; | ||||
var len = tb.length; | ||||
for (var i=0; i<len; i++) { | ||||
s = s + tb[i] + '\n'; | ||||
} | ||||
s = s + '\n'; | ||||
var toinsert = this.create_output_area(); | ||||
|
r15783 | var append_text = OutputArea.append_map['text/plain']; | ||
if (append_text) { | ||||
|
r16589 | append_text.apply(this, [s, {}, toinsert]).addClass('output_error'); | ||
|
r15783 | } | ||
|
r12332 | this._safe_append(toinsert); | ||
|
r9555 | } | ||
|
r7177 | }; | ||
OutputArea.prototype.append_stream = function (json) { | ||||
|
r18588 | var text = json.text; | ||
|
r19928 | if (typeof text !== 'string') { | ||
console.error("Stream output is invalid (missing text)", json); | ||||
return false; | ||||
} | ||||
|
r18584 | var subclass = "output_"+json.name; | ||
|
r7177 | if (this.outputs.length > 0){ | ||
// have at least one output to consider | ||||
var last = this.outputs[this.outputs.length-1]; | ||||
|
r18584 | if (last.output_type == 'stream' && json.name == last.name){ | ||
|
r7177 | // latest output was in the same stream, | ||
// so append directly into its pre tag | ||||
// escape ANSI & HTML specials: | ||||
|
r18588 | last.text = utils.fixCarriageReturn(last.text + json.text); | ||
|
r7350 | var pre = this.element.find('div.'+subclass).last().find('pre'); | ||
|
r18588 | var html = utils.fixConsole(last.text); | ||
|
r20435 | html = utils.autoLinkUrls(html); | ||
|
r15406 | // The only user content injected with this HTML call is | ||
|
r15376 | // escaped by the fixConsole() method. | ||
|
r7339 | pre.html(html); | ||
|
r17305 | // return false signals that we merged this output with the previous one, | ||
// and the new output shouldn't be recorded. | ||||
return false; | ||||
|
r7177 | } | ||
} | ||||
|
r7339 | |||
if (!text.replace("\r", "")) { | ||||
// text is nothing (empty string, \r, etc.) | ||||
// so don't append any elements, which might add undesirable space | ||||
|
r17305 | // return true to indicate the output should be recorded. | ||
return true; | ||||
|
r7339 | } | ||
|
r7177 | // If we got here, attach a new div | ||
var toinsert = this.create_output_area(); | ||||
|
r15783 | var append_text = OutputArea.append_map['text/plain']; | ||
if (append_text) { | ||||
append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass); | ||||
} | ||||
|
r12332 | this._safe_append(toinsert); | ||
|
r17305 | return true; | ||
|
r7177 | }; | ||
|
r18999 | OutputArea.prototype.append_unrecognized = function (json) { | ||
|
r19107 | var that = this; | ||
|
r18999 | var toinsert = this.create_output_area(); | ||
var subarea = $('<div/>').addClass('output_subarea output_unrecognized'); | ||||
toinsert.append(subarea); | ||||
|
r19107 | subarea.append( | ||
$("<a>") | ||||
.attr("href", "#") | ||||
.text("Unrecognized output: " + json.output_type) | ||||
.click(function () { | ||||
|
r20058 | that.events.trigger('unrecognized_output.OutputArea', {output: json}); | ||
|
r19107 | }) | ||
); | ||||
|
r18999 | this._safe_append(toinsert); | ||
}; | ||||
|
r16187 | OutputArea.prototype.append_display_data = function (json, handle_inserted) { | ||
|
r7177 | var toinsert = this.create_output_area(); | ||
|
r16187 | if (this.append_mime_type(json, toinsert, handle_inserted)) { | ||
|
r13454 | this._safe_append(toinsert); | ||
// If we just output latex, typeset it. | ||||
|
r18939 | if ((json.data['text/latex'] !== undefined) || | ||
(json.data['text/html'] !== undefined) || | ||||
(json.data['text/markdown'] !== undefined)) { | ||||
|
r13454 | this.typeset(); | ||
} | ||||
|
r9555 | } | ||
|
r7177 | }; | ||
|
r14856 | OutputArea.safe_outputs = { | ||
'text/plain' : true, | ||||
|
r15643 | 'text/latex' : true, | ||
|
r14856 | 'image/png' : true, | ||
'image/jpeg' : true | ||||
}; | ||||
|
r16187 | OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) { | ||
|
r16226 | for (var i=0; i < OutputArea.display_order.length; i++) { | ||
var type = OutputArea.display_order[i]; | ||||
|
r14135 | var append = OutputArea.append_map[type]; | ||
|
r18592 | if ((json.data[type] !== undefined) && append) { | ||
var value = json.data[type]; | ||||
|
r14856 | if (!this.trusted && !OutputArea.safe_outputs[type]) { | ||
|
r15644 | // not trusted, sanitize HTML | ||
|
r15634 | if (type==='text/html' || type==='text/svg') { | ||
|
r17198 | value = security.sanitize_html(value); | ||
|
r15644 | } else { | ||
|
r15658 | // don't display if we don't know how to sanitize it | ||
|
r15661 | console.log("Ignoring untrusted " + type + " output."); | ||
|
r15728 | continue; | ||
|
r14856 | } | ||
} | ||||
|
r14140 | var md = json.metadata || {}; | ||
|
r16187 | var toinsert = append.apply(this, [value, md, element, handle_inserted]); | ||
// Since only the png and jpeg mime types call the inserted | ||||
|
r16191 | // callback, if the mime type is something other we must call the | ||
|
r16187 | // inserted callback only when the element is actually inserted | ||
// into the DOM. Use a timeout of 0 to do this. | ||||
|
r16189 | if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) { | ||
|
r16187 | setTimeout(handle_inserted, 0); | ||
} | ||||
|
r17198 | this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]); | ||
|
r15729 | return toinsert; | ||
|
r9540 | } | ||
} | ||||
|
r15729 | return null; | ||
|
r7177 | }; | ||
|
r15762 | var append_html = function (html, md, element) { | ||
|
r14140 | var type = 'text/html'; | ||
|
r14116 | var toinsert = this.create_output_subarea(md, "output_html rendered_html", type); | ||
|
r17198 | this.keyboard_manager.register_events(toinsert); | ||
|
r7177 | toinsert.append(html); | ||
|
r21017 | dblclick_to_reset_size(toinsert.find('img')); | ||
|
r7177 | element.append(toinsert); | ||
|
r14883 | return toinsert; | ||
|
r7177 | }; | ||
|
r16364 | var append_markdown = function(markdown, md, element) { | ||
var type = 'text/markdown'; | ||||
var toinsert = this.create_output_subarea(md, "output_markdown", type); | ||||
|
r17198 | var text_and_math = mathjaxutils.remove_math(markdown); | ||
|
r16364 | var text = text_and_math[0]; | ||
var math = text_and_math[1]; | ||||
|
r18864 | marked(text, function (err, html) { | ||
html = mathjaxutils.replace_math(html, math); | ||||
toinsert.append(html); | ||||
}); | ||||
|
r21017 | dblclick_to_reset_size(toinsert.find('img')); | ||
|
r16364 | element.append(toinsert); | ||
return toinsert; | ||||
}; | ||||
|
r15762 | var append_javascript = function (js, md, element) { | ||
|
r19176 | /** | ||
* We just eval the JS code, element appears in the local scope. | ||||
*/ | ||||
|
r14140 | var type = 'application/javascript'; | ||
|
r14884 | var toinsert = this.create_output_subarea(md, "output_javascript", type); | ||
|
r17198 | this.keyboard_manager.register_events(toinsert); | ||
|
r14884 | element.append(toinsert); | ||
|
r15992 | |||
// Fix for ipython/issues/5293, make sure `element` is the area which | ||||
// output can be inserted into at the time of JS execution. | ||||
element = toinsert; | ||||
|
r8071 | try { | ||
eval(js); | ||||
} catch(err) { | ||||
|
r13792 | console.log(err); | ||
|
r14884 | this._append_javascript_error(err, toinsert); | ||
|
r8071 | } | ||
|
r14884 | return toinsert; | ||
|
r9555 | }; | ||
|
r7177 | |||
|
r15762 | var append_text = function (data, md, element) { | ||
|
r14140 | var type = 'text/plain'; | ||
|
r14116 | var toinsert = this.create_output_subarea(md, "output_text", type); | ||
|
r7177 | // escape ANSI & HTML specials in plaintext: | ||
data = utils.fixConsole(data); | ||||
|
r7339 | data = utils.fixCarriageReturn(data); | ||
|
r8528 | data = utils.autoLinkUrls(data); | ||
|
r15406 | // The only user content injected with this HTML call is | ||
|
r15376 | // escaped by the fixConsole() method. | ||
|
r7177 | toinsert.append($("<pre/>").html(data)); | ||
element.append(toinsert); | ||||
|
r14883 | return toinsert; | ||
|
r7177 | }; | ||
|
r16203 | var append_svg = function (svg_html, md, element) { | ||
|
r14140 | var type = 'image/svg+xml'; | ||
|
r14116 | var toinsert = this.create_output_subarea(md, "output_svg", type); | ||
|
r16203 | |||
// Get the svg element from within the HTML. | ||||
var svg = $('<div />').html(svg_html).find('svg'); | ||||
var svg_area = $('<div />'); | ||||
var width = svg.attr('width'); | ||||
var height = svg.attr('height'); | ||||
svg | ||||
.width('100%') | ||||
.height('100%'); | ||||
svg_area | ||||
.width(width) | ||||
.height(height); | ||||
svg_area.append(svg); | ||||
toinsert.append(svg_area); | ||||
|
r7355 | element.append(toinsert); | ||
|
r16203 | |||
|
r14883 | return toinsert; | ||
|
r7177 | }; | ||
|
r21017 | function dblclick_to_reset_size (img) { | ||
/** | ||||
* Double-click on an image toggles confinement to notebook width | ||||
* | ||||
* img: jQuery element | ||||
*/ | ||||
img.dblclick(function () { | ||||
// dblclick toggles *raw* size, disabling max-width confinement. | ||||
if (img.hasClass('unconfined')) { | ||||
img.removeClass('unconfined'); | ||||
} else { | ||||
img.addClass('unconfined'); | ||||
} | ||||
}); | ||||
}; | ||||
|
r14727 | var set_width_height = function (img, md, mime) { | ||
|
r19176 | /** | ||
* set width and height of an img element from metadata | ||||
*/ | ||||
|
r14727 | var height = _get_metadata_key(md, 'height', mime); | ||
|
r14807 | if (height !== undefined) img.attr('height', height); | ||
|
r14727 | var width = _get_metadata_key(md, 'width', mime); | ||
|
r14807 | if (width !== undefined) img.attr('width', width); | ||
|
r21017 | if (_get_metadata_key(md, 'unconfined', mime)) { | ||
img.addClass('unconfined'); | ||||
} | ||||
|
r14727 | }; | ||
|
r16187 | var append_png = function (png, md, element, handle_inserted) { | ||
|
r14140 | var type = 'image/png'; | ||
|
r14116 | var toinsert = this.create_output_subarea(md, "output_png", type); | ||
|
r16189 | var img = $("<img/>"); | ||
if (handle_inserted !== undefined) { | ||||
|
r16191 | img.on('load', function(){ | ||
handle_inserted(img); | ||||
}); | ||||
|
r16187 | } | ||
|
r16190 | img[0].src = 'data:image/png;base64,'+ png; | ||
|
r14807 | set_width_height(img, md, 'image/png'); | ||
|
r21017 | dblclick_to_reset_size(img); | ||
|
r7352 | toinsert.append(img); | ||
|
r7177 | element.append(toinsert); | ||
|
r14883 | return toinsert; | ||
|
r7177 | }; | ||
|
r16187 | var append_jpeg = function (jpeg, md, element, handle_inserted) { | ||
|
r14140 | var type = 'image/jpeg'; | ||
|
r14116 | var toinsert = this.create_output_subarea(md, "output_jpeg", type); | ||
|
r16189 | var img = $("<img/>"); | ||
if (handle_inserted !== undefined) { | ||||
|
r16191 | img.on('load', function(){ | ||
handle_inserted(img); | ||||
}); | ||||
|
r16187 | } | ||
|
r16190 | img[0].src = 'data:image/jpeg;base64,'+ jpeg; | ||
|
r14807 | set_width_height(img, md, 'image/jpeg'); | ||
|
r21017 | dblclick_to_reset_size(img); | ||
|
r7352 | toinsert.append(img); | ||
|
r7177 | element.append(toinsert); | ||
|
r14883 | return toinsert; | ||
|
r7177 | }; | ||
|
r15762 | var append_pdf = function (pdf, md, element) { | ||
|
r15127 | var type = 'application/pdf'; | ||
var toinsert = this.create_output_subarea(md, "output_pdf", type); | ||||
var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf); | ||||
a.attr('target', '_blank'); | ||||
|
r18591 | a.text('View PDF'); | ||
|
r15127 | toinsert.append(a); | ||
element.append(toinsert); | ||||
return toinsert; | ||||
|
r18591 | }; | ||
|
r15127 | |||
|
r15762 | var append_latex = function (latex, md, element) { | ||
|
r19176 | /** | ||
* This method cannot do the typesetting because the latex first has to | ||||
* be on the page. | ||||
*/ | ||||
|
r14140 | var type = 'text/latex'; | ||
|
r14116 | var toinsert = this.create_output_subarea(md, "output_latex", type); | ||
|
r7177 | toinsert.append(latex); | ||
element.append(toinsert); | ||||
|
r14883 | return toinsert; | ||
|
r7177 | }; | ||
|
r10776 | |||
|
r14132 | |||
|
r13207 | OutputArea.prototype.append_raw_input = function (msg) { | ||
|
r10370 | var that = this; | ||
|
r10368 | this.expand(); | ||
|
r13207 | var content = msg.content; | ||
|
r10368 | var area = this.create_output_area(); | ||
|
r11612 | |||
// disable any other raw_inputs, if they are left around | ||||
|
r15825 | $("div.output_subarea.raw_input_container").remove(); | ||
|
r11612 | |||
|
r16573 | var input_type = content.password ? 'password' : 'text'; | ||
|
r10368 | area.append( | ||
$("<div/>") | ||||
|
r15825 | .addClass("box-flex1 output_subarea raw_input_container") | ||
|
r10368 | .append( | ||
|
r10370 | $("<span/>") | ||
|
r15825 | .addClass("raw_input_prompt") | ||
|
r10370 | .text(content.prompt) | ||
) | ||||
.append( | ||||
$("<input/>") | ||||
.addClass("raw_input") | ||||
|
r16573 | .attr('type', input_type) | ||
|
r10934 | .attr("size", 47) | ||
|
r10370 | .keydown(function (event, ui) { | ||
// make sure we submit on enter, | ||||
// and don't re-execute the *cell* on shift-enter | ||||
|
r17198 | if (event.which === keyboard.keycodes.enter) { | ||
|
r10370 | that._submit_raw_input(); | ||
return false; | ||||
} | ||||
}) | ||||
|
r10368 | ) | ||
|
r10370 | ); | ||
|
r14046 | |||
|
r10368 | this.element.append(area); | ||
|
r14046 | var raw_input = area.find('input.raw_input'); | ||
// Register events that enable/disable the keyboard manager while raw | ||||
// input is focused. | ||||
|
r17198 | this.keyboard_manager.register_events(raw_input); | ||
|
r14046 | // Note, the following line used to read raw_input.focus().focus(). | ||
// This seemed to be needed otherwise only the cell would be focused. | ||||
// But with the modal UI, this seems to work fine with one call to focus(). | ||||
raw_input.focus(); | ||||
|
r18591 | }; | ||
|
r14046 | |||
|
r10368 | OutputArea.prototype._submit_raw_input = function (evt) { | ||
|
r15825 | var container = this.element.find("div.raw_input_container"); | ||
var theprompt = container.find("span.raw_input_prompt"); | ||||
|
r10368 | var theinput = container.find("input.raw_input"); | ||
|
r10934 | var value = theinput.val(); | ||
|
r16573 | var echo = value; | ||
// don't echo if it's a password | ||||
if (theinput.attr('type') == 'password') { | ||||
echo = '········'; | ||||
} | ||||
|
r10368 | var content = { | ||
output_type : 'stream', | ||||
|
r18981 | name : 'stdout', | ||
|
r16573 | text : theprompt.text() + echo + '\n' | ||
|
r18591 | }; | ||
|
r10368 | // remove form container | ||
container.parent().remove(); | ||||
// replace with plaintext version in stdout | ||||
this.append_output(content, false); | ||||
|
r17198 | this.events.trigger('send_input_reply.Kernel', value); | ||
|
r18592 | }; | ||
|
r7177 | |||
|
r13207 | OutputArea.prototype.handle_clear_output = function (msg) { | ||
|
r19176 | /** | ||
* msg spec v4 had stdout, stderr, display keys | ||||
* v4.1 replaced these with just wait | ||||
* The default behavior is the same (stdout=stderr=display=True, wait=False), | ||||
* so v4 messages will still be properly handled, | ||||
* except for the rarely used clearing less than all output. | ||||
*/ | ||||
|
r14977 | this.clear_output(msg.content.wait || false); | ||
|
r9555 | }; | ||
|
r7177 | |||
|
r19769 | OutputArea.prototype.clear_output = function(wait, ignore_que) { | ||
|
r12592 | if (wait) { | ||
|
r12582 | |||
|
r12592 | // If a clear is queued, clear before adding another to the queue. | ||
if (this.clear_queued) { | ||||
this.clear_output(false); | ||||
|
r18591 | } | ||
|
r12592 | |||
this.clear_queued = true; | ||||
} else { | ||||
|
r12606 | // Fix the output div's height if the clear_output is waiting for | ||
// new output (it is being used in an animation). | ||||
|
r19769 | if (!ignore_que && this.clear_queued) { | ||
|
r12606 | var height = this.element.height(); | ||
this.element.height(height); | ||||
this.clear_queued = false; | ||||
} | ||||
|
r16191 | // Clear all | ||
// Remove load event handlers from img tags because we don't want | ||||
// them to fire if the image is never added to the page. | ||||
this.element.find('img').off('load'); | ||||
|
r12592 | this.element.html(""); | ||
|
r18954 | |||
// Notify others of changes. | ||||
this.element.trigger('changed'); | ||||
|
r12592 | this.outputs = []; | ||
|
r14856 | this.trusted = true; | ||
|
r12592 | this.unscroll_area(); | ||
return; | ||||
|
r18591 | } | ||
|
r7177 | }; | ||
// JSON serialization | ||||
|
r18584 | OutputArea.prototype.fromJSON = function (outputs, metadata) { | ||
|
r7177 | var len = outputs.length; | ||
|
r18584 | metadata = metadata || {}; | ||
|
r14134 | |||
|
r7177 | for (var i=0; i<len; i++) { | ||
|
r18584 | this.append_output(outputs[i]); | ||
} | ||||
if (metadata.collapsed !== undefined) { | ||||
if (metadata.collapsed) { | ||||
|
r20058 | this.collapse(); | ||
|
r20253 | } else { | ||
this.expand(); | ||||
|
r16589 | } | ||
|
r18584 | } | ||
|
r20058 | if (metadata.scrolled !== undefined) { | ||
this.scroll_state = metadata.scrolled; | ||||
if (metadata.scrolled) { | ||||
this.scroll_if_long(); | ||||
|
r18584 | } else { | ||
|
r20058 | this.unscroll_area(); | ||
|
r14120 | } | ||
|
r9555 | } | ||
|
r7177 | }; | ||
OutputArea.prototype.toJSON = function () { | ||||
|
r18584 | return this.outputs; | ||
|
r7177 | }; | ||
|
r14985 | /** | ||
* Class properties | ||||
**/ | ||||
/** | ||||
* 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; | ||||
OutputArea.display_order = [ | ||||
'application/javascript', | ||||
'text/html', | ||||
|
r16364 | 'text/markdown', | ||
|
r14985 | 'text/latex', | ||
'image/svg+xml', | ||||
'image/png', | ||||
'image/jpeg', | ||||
|
r15127 | 'application/pdf', | ||
|
r14985 | 'text/plain' | ||
]; | ||||
OutputArea.append_map = { | ||||
|
r15762 | "text/plain" : append_text, | ||
"text/html" : append_html, | ||||
|
r16364 | "text/markdown": append_markdown, | ||
|
r15762 | "image/svg+xml" : append_svg, | ||
"image/png" : append_png, | ||||
"image/jpeg" : append_jpeg, | ||||
"text/latex" : append_latex, | ||||
"application/javascript" : append_javascript, | ||||
"application/pdf" : append_pdf | ||||
|
r14985 | }; | ||
|
r7177 | |||
|
r17198 | // For backwards compatability. | ||
|
r7177 | IPython.OutputArea = OutputArea; | ||
|
r17201 | return {'OutputArea': OutputArea}; | ||
|
r17198 | }); | ||