626 lines
22 KiB
JavaScript
626 lines
22 KiB
JavaScript
(function() {
|
|
// If window.HTMLWidgets is already defined, then use it; otherwise create a
|
|
// new object. This allows preceding code to set options that affect the
|
|
// initialization process (though none currently exist).
|
|
window.HTMLWidgets = window.HTMLWidgets || {};
|
|
|
|
// See if we're running in a viewer pane. If not, we're in a web browser.
|
|
var viewerMode = window.HTMLWidgets.viewerMode =
|
|
/\bviewer_pane=1\b/.test(window.location);
|
|
|
|
// See if we're running in Shiny mode. If not, it's a static document.
|
|
// Note that static widgets can appear in both Shiny and static modes, but
|
|
// obviously, Shiny widgets can only appear in Shiny apps/documents.
|
|
var shinyMode = window.HTMLWidgets.shinyMode =
|
|
typeof(window.Shiny) !== "undefined" && !!window.Shiny.outputBindings;
|
|
|
|
// We can't count on jQuery being available, so we implement our own
|
|
// version if necessary.
|
|
function querySelectorAll(scope, selector) {
|
|
if (typeof(jQuery) !== "undefined" && scope instanceof jQuery) {
|
|
return scope.find(selector);
|
|
}
|
|
if (scope.querySelectorAll) {
|
|
return scope.querySelectorAll(selector);
|
|
}
|
|
}
|
|
|
|
function asArray(value) {
|
|
if (value === null)
|
|
return [];
|
|
if ($.isArray(value))
|
|
return value;
|
|
return [value];
|
|
}
|
|
|
|
// Implement jQuery's extend
|
|
function extend(target /*, ... */) {
|
|
if (arguments.length == 1) {
|
|
return target;
|
|
}
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
var source = arguments[i];
|
|
for (var prop in source) {
|
|
if (source.hasOwnProperty(prop)) {
|
|
target[prop] = source[prop];
|
|
}
|
|
}
|
|
}
|
|
return target;
|
|
}
|
|
|
|
// IE8 doesn't support Array.forEach.
|
|
function forEach(values, callback, thisArg) {
|
|
if (values.forEach) {
|
|
values.forEach(callback, thisArg);
|
|
} else {
|
|
for (var i = 0; i < values.length; i++) {
|
|
callback.call(thisArg, values[i], i, values);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replaces the specified method with the return value of funcSource.
|
|
//
|
|
// Note that funcSource should not BE the new method, it should be a function
|
|
// that RETURNS the new method. funcSource receives a single argument that is
|
|
// the overridden method, it can be called from the new method. The overridden
|
|
// method can be called like a regular function, it has the target permanently
|
|
// bound to it so "this" will work correctly.
|
|
function overrideMethod(target, methodName, funcSource) {
|
|
var superFunc = target[methodName] || function() {};
|
|
var superFuncBound = function() {
|
|
return superFunc.apply(target, arguments);
|
|
};
|
|
target[methodName] = funcSource(superFuncBound);
|
|
}
|
|
|
|
// Implement a vague facsimilie of jQuery's data method
|
|
function elementData(el, name, value) {
|
|
if (arguments.length == 2) {
|
|
return el["htmlwidget_data_" + name];
|
|
} else if (arguments.length == 3) {
|
|
el["htmlwidget_data_" + name] = value;
|
|
return el;
|
|
} else {
|
|
throw new Error("Wrong number of arguments for elementData: " +
|
|
arguments.length);
|
|
}
|
|
}
|
|
|
|
// http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
|
function escapeRegExp(str) {
|
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
|
}
|
|
|
|
function hasClass(el, className) {
|
|
var re = new RegExp("\\b" + escapeRegExp(className) + "\\b");
|
|
return re.test(el.className);
|
|
}
|
|
|
|
// elements - array (or array-like object) of HTML elements
|
|
// className - class name to test for
|
|
// include - if true, only return elements with given className;
|
|
// if false, only return elements *without* given className
|
|
function filterByClass(elements, className, include) {
|
|
var results = [];
|
|
for (var i = 0; i < elements.length; i++) {
|
|
if (hasClass(elements[i], className) == include)
|
|
results.push(elements[i]);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function on(obj, eventName, func) {
|
|
if (obj.addEventListener) {
|
|
obj.addEventListener(eventName, func, false);
|
|
} else if (obj.attachEvent) {
|
|
obj.attachEvent(eventName, func);
|
|
}
|
|
}
|
|
|
|
function off(obj, eventName, func) {
|
|
if (obj.removeEventListener)
|
|
obj.removeEventListener(eventName, func, false);
|
|
else if (obj.detachEvent) {
|
|
obj.detachEvent(eventName, func);
|
|
}
|
|
}
|
|
|
|
// Translate array of values to top/right/bottom/left, as usual with
|
|
// the "padding" CSS property
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/padding
|
|
function unpackPadding(value) {
|
|
if (typeof(value) === "number")
|
|
value = [value];
|
|
if (value.length === 1) {
|
|
return {top: value[0], right: value[0], bottom: value[0], left: value[0]};
|
|
}
|
|
if (value.length === 2) {
|
|
return {top: value[0], right: value[1], bottom: value[0], left: value[1]};
|
|
}
|
|
if (value.length === 3) {
|
|
return {top: value[0], right: value[1], bottom: value[2], left: value[1]};
|
|
}
|
|
if (value.length === 4) {
|
|
return {top: value[0], right: value[1], bottom: value[2], left: value[3]};
|
|
}
|
|
}
|
|
|
|
// Convert an unpacked padding object to a CSS value
|
|
function paddingToCss(paddingObj) {
|
|
return paddingObj.top + "px " + paddingObj.right + "px " + paddingObj.bottom + "px " + paddingObj.left + "px";
|
|
}
|
|
|
|
// Makes a number suitable for CSS
|
|
function px(x) {
|
|
if (typeof(x) === "number")
|
|
return x + "px";
|
|
else
|
|
return x;
|
|
}
|
|
|
|
// Retrieves runtime widget sizing information for an element.
|
|
// The return value is either null, or an object with fill, padding,
|
|
// defaultWidth, defaultHeight fields.
|
|
function sizingPolicy(el) {
|
|
var sizingEl = document.querySelector("script[data-for='" + el.id + "'][type='application/htmlwidget-sizing']");
|
|
if (!sizingEl)
|
|
return null;
|
|
var sp = JSON.parse(sizingEl.textContent || sizingEl.text || "{}");
|
|
if (viewerMode) {
|
|
return sp.viewer;
|
|
} else {
|
|
return sp.browser;
|
|
}
|
|
}
|
|
|
|
function initSizing(el) {
|
|
var sizing = sizingPolicy(el);
|
|
if (!sizing)
|
|
return;
|
|
|
|
var cel = document.getElementById("htmlwidget_container");
|
|
if (!cel)
|
|
return;
|
|
|
|
if (typeof(sizing.padding) !== "undefined") {
|
|
document.body.style.margin = "0";
|
|
document.body.style.padding = paddingToCss(unpackPadding(sizing.padding));
|
|
}
|
|
|
|
if (sizing.fill) {
|
|
document.body.style.overflow = "hidden";
|
|
document.body.style.width = "100%";
|
|
document.body.style.height = "100%";
|
|
document.documentElement.style.width = "100%";
|
|
document.documentElement.style.height = "100%";
|
|
if (cel) {
|
|
cel.style.position = "absolute";
|
|
var pad = unpackPadding(sizing.padding);
|
|
cel.style.top = pad.top + "px";
|
|
cel.style.right = pad.right + "px";
|
|
cel.style.bottom = pad.bottom + "px";
|
|
cel.style.left = pad.left + "px";
|
|
el.style.width = "100%";
|
|
el.style.height = "100%";
|
|
}
|
|
|
|
return {
|
|
getWidth: function() { return cel.offsetWidth; },
|
|
getHeight: function() { return cel.offsetHeight; }
|
|
};
|
|
|
|
} else {
|
|
el.style.width = px(sizing.width);
|
|
el.style.height = px(sizing.height);
|
|
|
|
return {
|
|
getWidth: function() { return el.offsetWidth; },
|
|
getHeight: function() { return el.offsetHeight; }
|
|
};
|
|
}
|
|
}
|
|
|
|
// Default implementations for methods
|
|
var defaults = {
|
|
find: function(scope) {
|
|
return querySelectorAll(scope, "." + this.name);
|
|
},
|
|
renderError: function(el, err) {
|
|
var $el = $(el);
|
|
|
|
this.clearError(el);
|
|
|
|
// Add all these error classes, as Shiny does
|
|
var errClass = "shiny-output-error";
|
|
if (err.type !== null) {
|
|
// use the classes of the error condition as CSS class names
|
|
errClass = errClass + " " + $.map(asArray(err.type), function(type) {
|
|
return errClass + "-" + type;
|
|
}).join(" ");
|
|
}
|
|
errClass = errClass + " htmlwidgets-error";
|
|
|
|
// Is el inline or block? If inline or inline-block, just display:none it
|
|
// and add an inline error.
|
|
var display = $el.css("display");
|
|
$el.data("restore-display-mode", display);
|
|
|
|
if (display === "inline" || display === "inline-block") {
|
|
$el.hide();
|
|
if (err.message !== "") {
|
|
var errorSpan = $("<span>").addClass(errClass);
|
|
errorSpan.text(err.message);
|
|
$el.after(errorSpan);
|
|
}
|
|
} else if (display === "block") {
|
|
// If block, add an error just after the el, set visibility:none on the
|
|
// el, and position the error to be on top of the el.
|
|
// Mark it with a unique ID and CSS class so we can remove it later.
|
|
$el.css("visibility", "hidden");
|
|
if (err.message !== "") {
|
|
var errorDiv = $("<div>").addClass(errClass).css("position", "absolute")
|
|
.css("top", el.offsetTop)
|
|
.css("left", el.offsetLeft)
|
|
// setting width can push out the page size, forcing otherwise
|
|
// unnecessary scrollbars to appear and making it impossible for
|
|
// the element to shrink; so use max-width instead
|
|
.css("maxWidth", el.offsetWidth)
|
|
.css("height", el.offsetHeight);
|
|
errorDiv.text(err.message);
|
|
$el.after(errorDiv);
|
|
|
|
// Really dumb way to keep the size/position of the error in sync with
|
|
// the parent element as the window is resized or whatever.
|
|
var intId = setInterval(function() {
|
|
if (!errorDiv[0].parentElement) {
|
|
clearInterval(intId);
|
|
return;
|
|
}
|
|
errorDiv
|
|
.css("top", el.offsetTop)
|
|
.css("left", el.offsetLeft)
|
|
.css("maxWidth", el.offsetWidth)
|
|
.css("height", el.offsetHeight);
|
|
}, 500);
|
|
}
|
|
}
|
|
},
|
|
clearError: function(el) {
|
|
var $el = $(el);
|
|
var display = $el.data("restore-display-mode");
|
|
$el.data("restore-display-mode", null);
|
|
|
|
if (display === "inline" || display === "inline-block") {
|
|
if (display)
|
|
$el.css("display", display);
|
|
$(el.nextSibling).filter(".htmlwidgets-error").remove();
|
|
} else if (display === "block"){
|
|
$el.css("visibility", "inherit");
|
|
$(el.nextSibling).filter(".htmlwidgets-error").remove();
|
|
}
|
|
},
|
|
sizing: {}
|
|
};
|
|
|
|
// Called by widget bindings to register a new type of widget. The definition
|
|
// object can contain the following properties:
|
|
// - name (required) - A string indicating the binding name, which will be
|
|
// used by default as the CSS classname to look for.
|
|
// - initialize (optional) - A function(el) that will be called once per
|
|
// widget element; if a value is returned, it will be passed as the third
|
|
// value to renderValue.
|
|
// - renderValue (required) - A function(el, data, initValue) that will be
|
|
// called with data. Static contexts will cause this to be called once per
|
|
// element; Shiny apps will cause this to be called multiple times per
|
|
// element, as the data changes.
|
|
window.HTMLWidgets.widget = function(definition) {
|
|
if (!definition.name) {
|
|
throw new Error("Widget must have a name");
|
|
}
|
|
if (!definition.type) {
|
|
throw new Error("Widget must have a type");
|
|
}
|
|
// Currently we only support output widgets
|
|
if (definition.type !== "output") {
|
|
throw new Error("Unrecognized widget type '" + definition.type + "'");
|
|
}
|
|
// TODO: Verify that .name is a valid CSS classname
|
|
if (!definition.renderValue) {
|
|
throw new Error("Widget must have a renderValue function");
|
|
}
|
|
|
|
// For static rendering (non-Shiny), use a simple widget registration
|
|
// scheme. We also use this scheme for Shiny apps/documents that also
|
|
// contain static widgets.
|
|
window.HTMLWidgets.widgets = window.HTMLWidgets.widgets || [];
|
|
// Merge defaults into the definition; don't mutate the original definition.
|
|
var staticBinding = extend({}, defaults, definition);
|
|
overrideMethod(staticBinding, "find", function(superfunc) {
|
|
return function(scope) {
|
|
var results = superfunc(scope);
|
|
// Filter out Shiny outputs, we only want the static kind
|
|
return filterByClass(results, "html-widget-output", false);
|
|
};
|
|
});
|
|
window.HTMLWidgets.widgets.push(staticBinding);
|
|
|
|
if (shinyMode) {
|
|
// Shiny is running. Register the definition as an output binding.
|
|
|
|
// Merge defaults into the definition; don't mutate the original definition.
|
|
// The base object is a Shiny output binding if we're running in Shiny mode,
|
|
// or an empty object if we're not.
|
|
var shinyBinding = extend(new Shiny.OutputBinding(), defaults, definition);
|
|
|
|
// Wrap renderValue to handle initialization, which unfortunately isn't
|
|
// supported natively by Shiny at the time of this writing.
|
|
|
|
// NB: shinyBinding.initialize may be undefined, as it's optional.
|
|
|
|
// Rename initialize to make sure it isn't called by a future version
|
|
// of Shiny that does support initialize directly.
|
|
shinyBinding._htmlwidgets_initialize = shinyBinding.initialize;
|
|
delete shinyBinding.initialize;
|
|
|
|
overrideMethod(shinyBinding, "find", function(superfunc) {
|
|
return function(scope) {
|
|
|
|
var results = superfunc(scope);
|
|
|
|
// Only return elements that are Shiny outputs, not static ones
|
|
var dynamicResults = results.filter(".html-widget-output");
|
|
|
|
// It's possible that whatever caused Shiny to think there might be
|
|
// new dynamic outputs, also caused there to be new static outputs.
|
|
// Since there might be lots of different htmlwidgets bindings, we
|
|
// schedule execution for later--no need to staticRender multiple
|
|
// times.
|
|
if (results.length !== dynamicResults.length)
|
|
scheduleStaticRender();
|
|
|
|
return dynamicResults;
|
|
};
|
|
});
|
|
|
|
overrideMethod(shinyBinding, "renderValue", function(superfunc) {
|
|
return function(el, data) {
|
|
// Resolve strings marked as javascript literals to objects
|
|
if (!(data.evals instanceof Array)) data.evals = [data.evals];
|
|
for (var i = 0; data.evals && i < data.evals.length; i++) {
|
|
window.HTMLWidgets.evaluateStringMember(data.x, data.evals[i]);
|
|
}
|
|
if (!this.renderOnNullValue) {
|
|
if (data.x === null) {
|
|
el.style.visibility = "hidden";
|
|
return;
|
|
} else {
|
|
el.style.visibility = "inherit";
|
|
}
|
|
}
|
|
if (!elementData(el, "initialized")) {
|
|
initSizing(el);
|
|
|
|
elementData(el, "initialized", true);
|
|
if (this._htmlwidgets_initialize) {
|
|
var result = this._htmlwidgets_initialize(el, el.offsetWidth,
|
|
el.offsetHeight);
|
|
elementData(el, "init_result", result);
|
|
}
|
|
}
|
|
Shiny.renderDependencies(data.deps);
|
|
superfunc(el, data.x, elementData(el, "init_result"));
|
|
};
|
|
});
|
|
|
|
overrideMethod(shinyBinding, "resize", function(superfunc) {
|
|
return function(el, width, height) {
|
|
// Shiny can call resize before initialize/renderValue have been
|
|
// called, which doesn't make sense for widgets.
|
|
if (elementData(el, "initialized")) {
|
|
superfunc(el, width, height, elementData(el, "init_result"));
|
|
}
|
|
};
|
|
});
|
|
|
|
Shiny.outputBindings.register(shinyBinding, shinyBinding.name);
|
|
}
|
|
};
|
|
|
|
var scheduleStaticRenderTimerId = null;
|
|
function scheduleStaticRender() {
|
|
if (!scheduleStaticRenderTimerId) {
|
|
scheduleStaticRenderTimerId = setTimeout(function() {
|
|
scheduleStaticRenderTimerId = null;
|
|
window.HTMLWidgets.staticRender();
|
|
}, 1);
|
|
}
|
|
}
|
|
|
|
// Render static widgets after the document finishes loading
|
|
// Statically render all elements that are of this widget's class
|
|
window.HTMLWidgets.staticRender = function() {
|
|
var bindings = window.HTMLWidgets.widgets || [];
|
|
forEach(bindings, function(binding) {
|
|
var matches = binding.find(document.documentElement);
|
|
forEach(matches, function(el) {
|
|
var sizeObj = initSizing(el, binding);
|
|
|
|
if (hasClass(el, "html-widget-static-bound"))
|
|
return;
|
|
el.className = el.className + " html-widget-static-bound";
|
|
|
|
var initResult;
|
|
if (binding.initialize) {
|
|
initResult = binding.initialize(el,
|
|
sizeObj ? sizeObj.getWidth() : el.offsetWidth,
|
|
sizeObj ? sizeObj.getHeight() : el.offsetHeight
|
|
);
|
|
}
|
|
|
|
if (binding.resize) {
|
|
var lastSize = {};
|
|
var resizeHandler = function(e) {
|
|
var size = {
|
|
w: sizeObj ? sizeObj.getWidth() : el.offsetWidth,
|
|
h: sizeObj ? sizeObj.getHeight() : el.offsetHeight
|
|
};
|
|
if (size.w === 0 && size.h === 0)
|
|
return;
|
|
if (size.w === lastSize.w && size.h === lastSize.h)
|
|
return;
|
|
lastSize = size;
|
|
binding.resize(el, size.w, size.h, initResult);
|
|
};
|
|
|
|
on(window, "resize", resizeHandler);
|
|
|
|
// This is needed for cases where we're running in a Shiny
|
|
// app, but the widget itself is not a Shiny output, but
|
|
// rather a simple static widget. One example of this is
|
|
// an rmarkdown document that has runtime:shiny and widget
|
|
// that isn't in a render function. Shiny only knows to
|
|
// call resize handlers for Shiny outputs, not for static
|
|
// widgets, so we do it ourselves.
|
|
if (window.jQuery) {
|
|
window.jQuery(document).on("shown", resizeHandler);
|
|
window.jQuery(document).on("hidden", resizeHandler);
|
|
}
|
|
|
|
// This is needed for the specific case of ioslides, which
|
|
// flips slides between display:none and display:block.
|
|
// Ideally we would not have to have ioslide-specific code
|
|
// here, but rather have ioslides raise a generic event,
|
|
// but the rmarkdown package just went to CRAN so the
|
|
// window to getting that fixed may be long.
|
|
if (window.addEventListener) {
|
|
// It's OK to limit this to window.addEventListener
|
|
// browsers because ioslides itself only supports
|
|
// such browsers.
|
|
on(document, "slideenter", resizeHandler);
|
|
on(document, "slideleave", resizeHandler);
|
|
}
|
|
}
|
|
|
|
var scriptData = document.querySelector("script[data-for='" + el.id + "'][type='application/json']");
|
|
if (scriptData) {
|
|
var data = JSON.parse(scriptData.textContent || scriptData.text);
|
|
// Resolve strings marked as javascript literals to objects
|
|
if (!(data.evals instanceof Array)) data.evals = [data.evals];
|
|
for (var k = 0; data.evals && k < data.evals.length; k++) {
|
|
window.HTMLWidgets.evaluateStringMember(data.x, data.evals[k]);
|
|
}
|
|
binding.renderValue(el, data.x, initResult);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Wait until after the document has loaded to render the widgets.
|
|
if (document.addEventListener) {
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
document.removeEventListener("DOMContentLoaded", arguments.callee, false);
|
|
window.HTMLWidgets.staticRender();
|
|
}, false);
|
|
} else if (document.attachEvent) {
|
|
document.attachEvent("onreadystatechange", function() {
|
|
if (document.readyState === "complete") {
|
|
document.detachEvent("onreadystatechange", arguments.callee);
|
|
window.HTMLWidgets.staticRender();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
window.HTMLWidgets.getAttachmentUrl = function(depname, key) {
|
|
// If no key, default to the first item
|
|
if (typeof(key) === "undefined")
|
|
key = 1;
|
|
|
|
var link = document.getElementById(depname + "-" + key + "-attachment");
|
|
if (!link) {
|
|
throw new Error("Attachment " + depname + "/" + key + " not found in document");
|
|
}
|
|
return link.getAttribute("href");
|
|
};
|
|
|
|
window.HTMLWidgets.dataframeToD3 = function(df) {
|
|
var names = [];
|
|
var length;
|
|
for (var name in df) {
|
|
if (df.hasOwnProperty(name))
|
|
names.push(name);
|
|
if (typeof(df[name]) !== "object" || typeof(df[name].length) === "undefined") {
|
|
throw new Error("All fields must be arrays");
|
|
} else if (typeof(length) !== "undefined" && length !== df[name].length) {
|
|
throw new Error("All fields must be arrays of the same length");
|
|
}
|
|
length = df[name].length;
|
|
}
|
|
var results = [];
|
|
var item;
|
|
for (var row = 0; row < length; row++) {
|
|
item = {};
|
|
for (var col = 0; col < names.length; col++) {
|
|
item[names[col]] = df[names[col]][row];
|
|
}
|
|
results.push(item);
|
|
}
|
|
return results;
|
|
};
|
|
|
|
window.HTMLWidgets.transposeArray2D = function(array) {
|
|
var newArray = array[0].map(function(col, i) {
|
|
return array.map(function(row) {
|
|
return row[i]
|
|
})
|
|
});
|
|
return newArray;
|
|
};
|
|
// Split value at splitChar, but allow splitChar to be escaped
|
|
// using escapeChar. Any other characters escaped by escapeChar
|
|
// will be included as usual (including escapeChar itself).
|
|
function splitWithEscape(value, splitChar, escapeChar) {
|
|
var results = [];
|
|
var escapeMode = false;
|
|
var currentResult = "";
|
|
for (var pos = 0; pos < value.length; pos++) {
|
|
if (!escapeMode) {
|
|
if (value[pos] === splitChar) {
|
|
results.push(currentResult);
|
|
currentResult = "";
|
|
} else if (value[pos] === escapeChar) {
|
|
escapeMode = true;
|
|
} else {
|
|
currentResult += value[pos];
|
|
}
|
|
} else {
|
|
currentResult += value[pos];
|
|
escapeMode = false;
|
|
}
|
|
}
|
|
if (currentResult !== "") {
|
|
results.push(currentResult);
|
|
}
|
|
return results;
|
|
}
|
|
// Function authored by Yihui/JJ Allaire
|
|
window.HTMLWidgets.evaluateStringMember = function(o, member) {
|
|
var parts = splitWithEscape(member, '.', '\\');
|
|
for (var i = 0, l = parts.length; i < l; i++) {
|
|
var part = parts[i];
|
|
// part may be a character or 'numeric' member name
|
|
if (o !== null && typeof o === "object" && part in o) {
|
|
if (i == (l - 1)) { // if we are at the end of the line then evalulate
|
|
if (typeof o[part] === "string")
|
|
o[part] = eval("(" + o[part] + ")");
|
|
} else { // otherwise continue to next embedded object
|
|
o = o[part];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
})();
|
|
|