From e1f9fd29ad1531814c8f7c4d039a66d6a38e21a0 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 14 Sep 2017 14:34:27 -0700 Subject: [PATCH] create a vdom output type --- .../static/notebook/js/object-to-preact.js | 108 + notebook/static/notebook/js/outputarea.js | 1868 +++++++++-------- 2 files changed, 1092 insertions(+), 884 deletions(-) create mode 100644 notebook/static/notebook/js/object-to-preact.js diff --git a/notebook/static/notebook/js/object-to-preact.js b/notebook/static/notebook/js/object-to-preact.js new file mode 100644 index 00000000000..241b322cfb8 --- /dev/null +++ b/notebook/static/notebook/js/object-to-preact.js @@ -0,0 +1,108 @@ +/** + * The original copy of this comes from + * https://github.com/remarkablemark/REON/blob/1f126e71c17f96daad518abffdb2c53b66b8b792/lib/object-to-react.js + * + * MIT License + * + * Copyright (c) 2016 Menglin "Mark" Xu + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +define([], function() { + "use strict"; + // Since apparently this is global rather than part of the UMD loading + var preact = window.preact; + + /** + * Convert an object to preact element(s). + * + * @param {Object} obj - The element object. + * @return {ReactElement} + */ + function objectToPreactElement(obj) { + // Pack args for preact.h + var args = []; + + if (!obj.tagName || typeof obj.tagName !== "string") { + throw new Error("Invalid tagName on ", JSON.stringify(obj, null, 2)); + } + if (!obj.attributes || typeof obj.attributes !== "object") { + throw new Error("Attributes must exist on a VDOM Object"); + } + + // `React.createElement` 1st argument: type + args[0] = obj.tagName; + args[1] = obj.attributes; + + const children = obj.children; + + if (children) { + if (Array.isArray(children)) { + // to be safe (although this should never happen) + if (args[1] === undefined) { + args[1] = null; + } + args = args.concat(arrayToPreactChildren(children)); + } else if (typeof children === "string") { + args[2] = children; + } else if (typeof children === "object") { + args[2] = objectToPreactElement(children); + } else { + console.warn("invalid vdom data passed", children); + } + } + + return preact.h.apply({}, args); + } + + /** + * Convert an array of items to Preact children. + * + * @param {Array} arr - The array. + * @return {Array} - The array of mixed values. + */ + function arrayToPreactChildren(arr) { + // similar to `props.children` + var result = []; + // child of `props.children` + + // iterate through the `children` + for (var i = 0, len = arr.length; i < len; i++) { + // child can have mixed values: text, Preact element, or array + const item = arr[i]; + if (Array.isArray(item)) { + result.push(arrayToPreactChildren(item)); + } else if (typeof item === "string") { + result.push(item); + } else if (typeof item === "object") { + const keyedItem = item; + item.key = i; + result.push(objectToPreactElement(keyedItem)); + } else { + console.warn("invalid vdom data passed", item); + } + } + + return result; + } + + return { objectToPreactElement }; +}); diff --git a/notebook/static/notebook/js/outputarea.js b/notebook/static/notebook/js/outputarea.js index 281215b6acc..447f8e9cec3 100644 --- a/notebook/static/notebook/js/outputarea.js +++ b/notebook/static/notebook/js/outputarea.js @@ -1,16 +1,31 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -define([ - 'jquery', - 'base/js/utils', - 'base/js/i18n', - 'base/js/security', - 'base/js/keyboard', - 'services/config', - 'notebook/js/mathjaxutils', - 'components/marked/lib/marked', -], function($, utils, i18n, security, keyboard, configmod, mathjaxutils, marked) { +define( + [ + "jquery", + "underscore", + "base/js/utils", + "base/js/i18n", + "base/js/security", + "base/js/keyboard", + "services/config", + "notebook/js/mathjaxutils", + "notebook/js/object-to-preact", + "components/marked/lib/marked" + ], + function( + $, + _, + utils, + i18n, + security, + keyboard, + configmod, + mathjaxutils, + otp, + marked + ) { "use strict"; /** @@ -19,76 +34,81 @@ define([ * @constructor */ - var OutputArea = function (options) { - this.config = options.config; - 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._display_id_targets = {}; - this.create_elements(); - this.style(); - this.bind_events(); - this.class_config = new configmod.ConfigWithDefaults(this.config, - OutputArea.config_defaults, 'OutputArea'); - - this.handle_appended = utils.throttle(this.handle_appended.bind(this)); + var OutputArea = function(options) { + this.config = options.config; + 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._display_id_targets = {}; + this.create_elements(); + this.style(); + this.bind_events(); + this.class_config = new configmod.ConfigWithDefaults( + this.config, + OutputArea.config_defaults, + "OutputArea" + ); + + this.handle_appended = utils.throttle(this.handle_appended.bind(this)); }; OutputArea.config_defaults = { - stream_chunk_size: 8192, // chunk size for stream output + stream_chunk_size: 8192 // chunk size for stream output }; - + /** * Class prototypes **/ - OutputArea.prototype.create_elements = function () { - var element = this.element = $("
"); - // wrap element in safe trigger, - // so that errors (e.g. in widget extensions) are logged instead of - // breaking everything. - this.element._original_trigger = this.element.trigger; - this.element.trigger = function (name, data) { - try { - this._original_trigger.apply(this, arguments); - } catch (e) { - console.error("Exception in event handler for " + name, e, arguments); - } + OutputArea.prototype.create_elements = function() { + var element = (this.element = $("
")); + // wrap element in safe trigger, + // so that errors (e.g. in widget extensions) are logged instead of + // breaking everything. + this.element._original_trigger = this.element.trigger; + this.element.trigger = function(name, data) { + try { + this._original_trigger.apply(this, arguments); + } catch (e) { + console.error("Exception in event handler for " + name, e, arguments); } - this.collapse_button = $("
"); - this.prompt_overlay = $("
"); - this.wrapper.append(this.prompt_overlay); - this.wrapper.append(this.element); - this.wrapper.append(this.collapse_button); + }; + 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(); - OutputArea.prototype.style = function () { - this.collapse_button.hide(); - - this.wrapper.addClass('output_wrapper'); - this.element.addClass('output'); - - this.collapse_button.addClass("btn btn-default output_collapsed"); - this.collapse_button.attr('title', i18n.msg._('click to expand output')); - this.collapse_button.text('. . .'); - - this.prompt_overlay.addClass('out_prompt_overlay prompt'); - this.prompt_overlay.attr('title', i18n.msg._('click to expand output; double click to hide output')); - - this.expand(); + this.wrapper.addClass("output_wrapper"); + this.element.addClass("output"); + + this.collapse_button.addClass("btn btn-default output_collapsed"); + this.collapse_button.attr("title", i18n.msg._("click to expand output")); + this.collapse_button.text(". . ."); + + this.prompt_overlay.addClass("out_prompt_overlay prompt"); + this.prompt_overlay.attr( + "title", + i18n.msg._("click to expand output; double click to hide output") + ); + + this.expand(); }; /** @@ -99,90 +119,96 @@ define([ * 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') || '14px'; - var lineHeight = Math.floor((parseFloat(fontSize.replace('px','')) || 14) * 1.3); - 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.on('resizeOutput', function () { - // 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._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") || "14px"; + var lineHeight = Math.floor( + (parseFloat(fontSize.replace("px", "")) || 14) * 1.3 + ); + return this.element.height() > threshold * lineHeight; }; - - 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.bind_events = function() { + var that = this; + this.prompt_overlay.dblclick(function() { + that.toggle_output(); + }); + this.prompt_overlay.click(function() { + that.toggle_scroll(); + }); + + this.element.on("resizeOutput", function() { + // 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.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.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.toggle_output = function () { - if (this.collapsed) { - this.expand(); - } else { - this.collapse(); + 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.scroll_area = function () { - this.element.addClass('output_scroll'); - this.prompt_overlay.attr('title', i18n.msg._('click to unscroll output; double click to hide')); - this.scrolled = true; + 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", + i18n.msg._("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', i18n.msg._('click to scroll output; double click to hide')); - this.scrolled = false; + OutputArea.prototype.unscroll_area = function() { + this.element.removeClass("output_scroll"); + this.prompt_overlay.attr( + "title", + i18n.msg._("click to scroll output; double click to hide") + ); + this.scrolled = false; }; /** @@ -192,468 +218,503 @@ define([ * 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.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(); - } + 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.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; - switch(msg_type) { - case "stream" : - json.text = content.text; - json.name = content.name; - break; + OutputArea.prototype.handle_output = function(msg) { + var json = {}; + var msg_type = (json.output_type = msg.header.msg_type); + var content = msg.content; + switch (msg_type) { + case "stream": + json.text = content.text; + json.name = content.name; + break; case "execute_result": - json.execution_count = content.execution_count; + json.execution_count = content.execution_count; case "update_display_data": case "display_data": - json.transient = content.transient; - json.data = content.data; - json.metadata = content.metadata; - break; + json.transient = content.transient; + json.data = content.data; + json.metadata = content.metadata; + break; case "error": - json.ename = content.ename; - json.evalue = content.evalue; - json.traceback = content.traceback; - break; + json.ename = content.ename; + json.evalue = content.evalue; + json.traceback = content.traceback; + break; default: - console.error("unhandled output message", msg); - return; - } - this.append_output(json); + console.error("unhandled output message", msg); + return; + } + this.append_output(json); }; - + // Declare mime type as constants - var MIME_JAVASCRIPT = 'application/javascript'; - var MIME_HTML = 'text/html'; - var MIME_MARKDOWN = 'text/markdown'; - var MIME_LATEX = 'text/latex'; - var MIME_SVG = 'image/svg+xml'; - var MIME_PNG = 'image/png'; - var MIME_JPEG = 'image/jpeg'; - var MIME_GIF = 'image/gif'; - var MIME_PDF = 'application/pdf'; - var MIME_TEXT = 'text/plain'; - - + var MIME_JAVASCRIPT = "application/javascript"; + var MIME_HTML = "text/html"; + var MIME_VDOM = "application/vdom.v1+json"; + var MIME_MARKDOWN = "text/markdown"; + var MIME_LATEX = "text/latex"; + var MIME_SVG = "image/svg+xml"; + var MIME_PNG = "image/png"; + var MIME_JPEG = "image/jpeg"; + var MIME_GIF = "image/gif"; + var MIME_PDF = "application/pdf"; + var MIME_TEXT = "text/plain"; + OutputArea.output_types = [ - MIME_JAVASCRIPT, - MIME_HTML, - MIME_MARKDOWN, - MIME_LATEX, - MIME_SVG, - MIME_PNG, - MIME_JPEG, - MIME_GIF, - MIME_PDF, - MIME_TEXT, + MIME_VDOM, + MIME_JAVASCRIPT, + MIME_HTML, + MIME_MARKDOWN, + MIME_LATEX, + MIME_SVG, + MIME_PNG, + MIME_JPEG, + MIME_GIF, + MIME_PDF, + MIME_TEXT ]; - 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 = {}; + 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.indexOf("application/") === -1 || key.indexOf("json") === -1) && + data[key] !== undefined && + typeof data[key] !== "string" + ) { + console.log("Invalid type for " + key, data[key]); + delete data[key]; } - var data = bundle.data; - $.map(OutputArea.output_types, function(key){ - if ((key.indexOf('application/') === -1 || key.indexOf('json') === -1) && - data[key] !== undefined && - typeof data[key] !== 'string' - ) { - console.log("Invalid type for " + key, data[key]); - delete data[key]; - } - }); - return bundle; + }); + return bundle; }; - - OutputArea.prototype.append_output = function (json) { - this.expand(); - - if (this.clear_queued) { - this.clear_output(false); - this._needs_height_reset = true; - } - var record_output = true; - switch(json.output_type) { - case 'update_display_data': - record_output = false; - json = this.validate_mimebundle(json); - this.update_display_data(json); - return; - 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); - } + OutputArea.prototype.append_output = function(json) { + this.expand(); - if (json.output_type === 'display_data') { - var that = this; - this.append_display_data(json, this.handle_appended); - } else { - this.handle_appended(); - } + if (this.clear_queued) { + this.clear_output(false); + this._needs_height_reset = true; + } - if (record_output) { - this.outputs.push(json); - } + var record_output = true; + switch (json.output_type) { + case "update_display_data": + record_output = false; + json = this.validate_mimebundle(json); + this.update_display_data(json); + return; + 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); + } - this.events.trigger('output_added.OutputArea', { - output: json, - output_area: this, - }); + if (json.output_type === "display_data") { + var that = this; + this.append_display_data(json, this.handle_appended); + } else { + this.handle_appended(); + } + + if (record_output) { + this.outputs.push(json); + } + + this.events.trigger("output_added.OutputArea", { + output: json, + output_area: this + }); }; - OutputArea.prototype.handle_appended = function () { - if (this._needs_height_reset) { - this.element.height(''); - this._needs_height_reset = false; - } + OutputArea.prototype.handle_appended = function() { + if (this._needs_height_reset) { + this.element.height(""); + this._needs_height_reset = false; + } - this.element.trigger('resizeOutput', {output_area: this}); + this.element.trigger("resizeOutput", { output_area: this }); }; - OutputArea.prototype.create_output_area = function () { - var oa = $("
").addClass("output_area"); - if (this.prompt_area) { - oa.append($('
').addClass('prompt')); - } - return oa; + 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]; + 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 = $('