'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
//---------------------------------------------------------------------
// Source file: ../srcjs/_start.js
(function () {
var $ = jQuery;
var exports = window.Shiny = window.Shiny || {};
$(document).on('submit', 'form:not([action])', function (e) {
e.preventDefault();
});
//---------------------------------------------------------------------
// Source file: ../srcjs/utils.js
function escapeHTML(str) {
return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/");
}
function randomId() {
return Math.floor(0x100000000 + Math.random() * 0xF00000000).toString(16);
}
function strToBool(str) {
if (!str || !str.toLowerCase) return undefined;
switch (str.toLowerCase()) {
case 'true':
return true;
case 'false':
return false;
default:
return undefined;
}
}
// A wrapper for getComputedStyle that is compatible with older browsers.
// This is significantly faster than jQuery's .css() function.
function getStyle(el, styleProp) {
var x;
if (el.currentStyle) x = el.currentStyle[styleProp];else if (window.getComputedStyle) {
// getComputedStyle can return null when we're inside a hidden iframe on
// Firefox; don't attempt to retrieve style props in this case.
// https://bugzilla.mozilla.org/show_bug.cgi?id=548397
var style = document.defaultView.getComputedStyle(el, null);
if (style) x = style.getPropertyValue(styleProp);
}
return x;
}
// Convert a number to a string with leading zeros
function padZeros(n, digits) {
var str = n.toString();
while (str.length < digits) {
str = "0" + str;
}return str;
}
// Take a string with format "YYYY-MM-DD" and return a Date object.
// IE8 and QTWebKit don't support YYYY-MM-DD, but they support YYYY/MM/DD
function parseDate(dateString) {
var date = new Date(dateString);
if (isNaN(date)) date = new Date(dateString.replace(/-/g, "/"));
return date;
}
// Given a Date object, return a string in yyyy-mm-dd format, using the
// UTC date. This may be a day off from the date in the local time zone.
function formatDateUTC(date) {
if (date instanceof Date) {
return date.getUTCFullYear() + '-' + padZeros(date.getUTCMonth() + 1, 2) + '-' + padZeros(date.getUTCDate(), 2);
} else {
return null;
}
}
// Given an element and a function(width, height), returns a function(). When
// the output function is called, it calls the input function with the offset
// width and height of the input element--but only if the size of the element
// is non-zero and the size is different than the last time the output
// function was called.
//
// Basically we are trying to filter out extraneous calls to func, so that
// when the window size changes or whatever, we don't run resize logic for
// elements that haven't actually changed size or aren't visible anyway.
function makeResizeFilter(el, func) {
var lastSize = {};
return function () {
var size = { w: el.offsetWidth, h: el.offsetHeight };
if (size.w === 0 && size.h === 0) return;
if (size.w === lastSize.w && size.h === lastSize.h) return;
lastSize = size;
func(size.w, size.h);
};
}
var _BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
function makeBlob(parts) {
// Browser compatibility is a mess right now. The code as written works in
// a variety of modern browsers, but sadly gives a deprecation warning
// message on the console in current versions (as of this writing) of
// Chrome.
// Safari 6.0 (8536.25) on Mac OS X 10.8.1:
// Has Blob constructor but it doesn't work with ArrayBufferView args
// Google Chrome 21.0.1180.81 on Xubuntu 12.04:
// Has Blob constructor, accepts ArrayBufferView args, accepts ArrayBuffer
// but with a deprecation warning message
// Firefox 15.0 on Xubuntu 12.04:
// Has Blob constructor, accepts both ArrayBuffer and ArrayBufferView args
// Chromium 18.0.1025.168 (Developer Build 134367 Linux) on Xubuntu 12.04:
// No Blob constructor. Has WebKitBlobBuilder.
try {
return new Blob(parts);
} catch (e) {
var blobBuilder = new _BlobBuilder();
$.each(parts, function (i, part) {
blobBuilder.append(part);
});
return blobBuilder.getBlob();
}
}
function pixelRatio() {
if (window.devicePixelRatio) {
return window.devicePixelRatio;
} else {
return 1;
}
}
// Takes a string expression and returns a function that takes an argument.
//
// When the function is executed, it will evaluate that expression using
// "with" on the argument value, and return the result.
function scopeExprToFunc(expr) {
/*jshint evil: true */
var func = new Function("with (this) {return (" + expr + ");}");
return function (scope) {
return func.call(scope);
};
}
function asArray(value) {
if (value === null || value === undefined) return [];
if ($.isArray(value)) return value;
return [value];
}
// We need a stable sorting algorithm for ordering
// bindings by priority and insertion order.
function mergeSort(list, sortfunc) {
function merge(sortfunc, a, b) {
var ia = 0;
var ib = 0;
var sorted = [];
while (ia < a.length && ib < b.length) {
if (sortfunc(a[ia], b[ib]) <= 0) {
sorted.push(a[ia++]);
} else {
sorted.push(b[ib++]);
}
}
while (ia < a.length) {
sorted.push(a[ia++]);
}while (ib < b.length) {
sorted.push(b[ib++]);
}return sorted;
}
// Don't mutate list argument
list = list.slice(0);
for (var chunkSize = 1; chunkSize < list.length; chunkSize *= 2) {
for (var i = 0; i < list.length; i += chunkSize * 2) {
var listA = list.slice(i, i + chunkSize);
var listB = list.slice(i + chunkSize, i + chunkSize * 2);
var merged = merge(sortfunc, listA, listB);
var args = [i, merged.length];
Array.prototype.push.apply(args, merged);
Array.prototype.splice.apply(list, args);
}
}
return list;
}
// Escape jQuery selector metacharacters: !"#$%&'()*+,./:;<=>?@[\]^`{|}~
var $escape = exports.$escape = function (val) {
return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
};
//---------------------------------------------------------------------
// Source file: ../srcjs/browser.js
var browser = function () {
var isQt = false;
// For easy handling of Qt quirks using CSS
if (/\bQt\//.test(window.navigator.userAgent)) {
$(document.documentElement).addClass('qt');
isQt = true;
}
// Enable special treatment for Qt 5 quirks on Linux
if (/\bQt\/5/.test(window.navigator.userAgent) && /Linux/.test(window.navigator.userAgent)) {
$(document.documentElement).addClass('qt5');
}
// Detect IE information
var isIE = navigator.appName === 'Microsoft Internet Explorer';
function getIEVersion() {
var rv = -1;
if (isIE) {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1);
}
return rv;
}
return {
isQt: isQt,
isIE: isIE,
IEVersion: getIEVersion()
};
}();
//---------------------------------------------------------------------
// Source file: ../srcjs/input_rate.js
var Invoker = function Invoker(target, func) {
this.target = target;
this.func = func;
};
(function () {
this.normalCall = this.immediateCall = function () {
this.func.apply(this.target, arguments);
};
}).call(Invoker.prototype);
var Debouncer = function Debouncer(target, func, delayMs) {
this.target = target;
this.func = func;
this.delayMs = delayMs;
this.timerId = null;
this.args = null;
};
(function () {
this.normalCall = function () {
var self = this;
this.$clearTimer();
this.args = arguments;
this.timerId = setTimeout(function () {
// IE8 doesn't reliably clear timeout, so this additional
// check is needed
if (self.timerId === null) return;
self.$clearTimer();
self.$invoke();
}, this.delayMs);
};
this.immediateCall = function () {
this.$clearTimer();
this.args = arguments;
this.$invoke();
};
this.isPending = function () {
return this.timerId !== null;
};
this.$clearTimer = function () {
if (this.timerId !== null) {
clearTimeout(this.timerId);
this.timerId = null;
}
};
this.$invoke = function () {
this.func.apply(this.target, this.args);
this.args = null;
};
}).call(Debouncer.prototype);
var Throttler = function Throttler(target, func, delayMs) {
this.target = target;
this.func = func;
this.delayMs = delayMs;
this.timerId = null;
this.args = null;
};
(function () {
this.normalCall = function () {
var self = this;
this.args = arguments;
if (this.timerId === null) {
this.$invoke();
this.timerId = setTimeout(function () {
// IE8 doesn't reliably clear timeout, so this additional
// check is needed
if (self.timerId === null) return;
self.$clearTimer();
if (self.args) self.normalCall.apply(self, self.args);
}, this.delayMs);
}
};
this.immediateCall = function () {
this.$clearTimer();
this.args = arguments;
this.$invoke();
};
this.isPending = function () {
return this.timerId !== null;
};
this.$clearTimer = function () {
if (this.timerId !== null) {
clearTimeout(this.timerId);
this.timerId = null;
}
};
this.$invoke = function () {
this.func.apply(this.target, this.args);
this.args = null;
};
}).call(Throttler.prototype);
// Returns a debounced version of the given function.
// Debouncing means that when the function is invoked,
// there is a delay of `threshold` milliseconds before
// it is actually executed, and if the function is
// invoked again before that threshold has elapsed then
// the clock starts over.
//
// For example, if a function is debounced with a
// threshold of 1000ms, then calling it 17 times at
// 900ms intervals will result in a single execution
// of the underlying function, 1000ms after the 17th
// call.
function debounce(threshold, func) {
var timerId = null;
var self, args;
return function () {
self = this;
args = arguments;
if (timerId !== null) {
clearTimeout(timerId);
timerId = null;
}
timerId = setTimeout(function () {
// IE8 doesn't reliably clear timeout, so this additional
// check is needed
if (timerId === null) return;
timerId = null;
func.apply(self, args);
}, threshold);
};
}
// Returns a throttled version of the given function.
// Throttling means that the underlying function will
// be executed no more than once every `threshold`
// milliseconds.
//
// For example, if a function is throttled with a
// threshold of 1000ms, then calling it 17 times at
// 900ms intervals will result in something like 15
// or 16 executions of the underlying function.
// eslint-disable-next-line no-unused-vars
function throttle(threshold, func) {
var executionPending = false;
var timerId = null;
var self, args;
function throttled() {
self = null;
args = null;
if (timerId === null) {
// Haven't seen a call recently. Execute now and
// start a timer to buffer any subsequent calls.
timerId = setTimeout(function () {
// When time expires, clear the timer; and if
// there has been a call in the meantime, repeat.
timerId = null;
if (executionPending) {
executionPending = false;
throttled.apply(self, args);
}
}, threshold);
func.apply(this, arguments);
} else {
// Something executed recently. Don't do anything
// except set up target/arguments to be called later
executionPending = true;
self = this;
args = arguments;
}
}
return throttled;
}
// Schedules data to be sent to shinyapp at the next setTimeout(0).
// Batches multiple input calls into one websocket message.
var InputBatchSender = function InputBatchSender(shinyapp) {
this.shinyapp = shinyapp;
this.timerId = null;
this.pendingData = {};
this.reentrant = false;
this.lastChanceCallback = [];
};
(function () {
this.setInput = function (name, value) {
var self = this;
this.pendingData[name] = value;
if (!this.timerId && !this.reentrant) {
this.timerId = setTimeout(function () {
self.reentrant = true;
try {
$.each(self.lastChanceCallback, function (i, callback) {
callback();
});
self.timerId = null;
var currentData = self.pendingData;
self.pendingData = {};
self.shinyapp.sendInput(currentData);
} finally {
self.reentrant = false;
}
}, 0);
}
};
}).call(InputBatchSender.prototype);
var InputNoResendDecorator = function InputNoResendDecorator(target, initialValues) {
this.target = target;
this.lastSentValues = initialValues || {};
};
(function () {
this.setInput = function (name, value) {
var jsonValue = JSON.stringify(value);
if (this.lastSentValues[name] === jsonValue) return;
this.lastSentValues[name] = jsonValue;
this.target.setInput(name, value);
};
this.reset = function (values) {
values = values || {};
var strValues = {};
$.each(values, function (key, value) {
strValues[key] = JSON.stringify(value);
});
this.lastSentValues = strValues;
};
}).call(InputNoResendDecorator.prototype);
var InputDeferDecorator = function InputDeferDecorator(target) {
this.target = target;
this.pendingInput = {};
};
(function () {
this.setInput = function (name, value) {
if (/^\./.test(name)) this.target.setInput(name, value);else this.pendingInput[name] = value;
};
this.submit = function () {
for (var name in this.pendingInput) {
if (this.pendingInput.hasOwnProperty(name)) this.target.setInput(name, this.pendingInput[name]);
}
};
}).call(InputDeferDecorator.prototype);
var InputEventDecorator = function InputEventDecorator(target) {
this.target = target;
};
(function () {
this.setInput = function (name, value, immediate) {
var evt = jQuery.Event("shiny:inputchanged");
var name2 = name.split(':');
evt.name = name2[0];
evt.inputType = name2.length > 1 ? name2[1] : '';
evt.value = value;
$(document).trigger(evt);
if (!evt.isDefaultPrevented()) {
name = evt.name;
if (evt.inputType !== '') name += ':' + evt.inputType;
this.target.setInput(name, evt.value, immediate);
}
};
}).call(InputEventDecorator.prototype);
var InputRateDecorator = function InputRateDecorator(target) {
this.target = target;
this.inputRatePolicies = {};
};
(function () {
this.setInput = function (name, value, immediate) {
this.$ensureInit(name);
if (immediate) this.inputRatePolicies[name].immediateCall(name, value, immediate);else this.inputRatePolicies[name].normalCall(name, value, immediate);
};
this.setRatePolicy = function (name, mode, millis) {
if (mode === 'direct') {
this.inputRatePolicies[name] = new Invoker(this, this.$doSetInput);
} else if (mode === 'debounce') {
this.inputRatePolicies[name] = new Debouncer(this, this.$doSetInput, millis);
} else if (mode === 'throttle') {
this.inputRatePolicies[name] = new Throttler(this, this.$doSetInput, millis);
}
};
this.$ensureInit = function (name) {
if (!(name in this.inputRatePolicies)) this.setRatePolicy(name, 'direct');
};
this.$doSetInput = function (name, value) {
this.target.setInput(name, value);
};
}).call(InputRateDecorator.prototype);
//---------------------------------------------------------------------
// Source file: ../srcjs/shinyapp.js
var ShinyApp = function ShinyApp() {
this.$socket = null;
// Cached input values
this.$inputValues = {};
// Output bindings
this.$bindings = {};
// Cached values/errors
this.$values = {};
this.$errors = {};
// Conditional bindings (show/hide element based on expression)
this.$conditionals = {};
this.$pendingMessages = [];
this.$activeRequests = {};
this.$nextRequestId = 0;
this.$allowReconnect = false;
};
(function () {
this.connect = function (initialInput) {
if (this.$socket) throw "Connect was already called on this application object";
$.extend(initialInput, {
// IE8 and IE9 have some limitations with data URIs
".clientdata_allowDataUriScheme": typeof WebSocket !== 'undefined'
});
this.$socket = this.createSocket();
this.$initialInput = initialInput;
$.extend(this.$inputValues, initialInput);
this.$updateConditionals();
};
this.isConnected = function () {
return !!this.$socket;
};
var scheduledReconnect = null;
this.reconnect = function () {
// This function can be invoked directly even if there's a scheduled
// reconnect, so be sure to clear any such scheduled reconnects.
clearTimeout(scheduledReconnect);
if (this.isConnected()) throw "Attempted to reconnect, but already connected.";
this.$socket = this.createSocket();
this.$initialInput = $.extend({}, this.$inputValues);
this.$updateConditionals();
};
this.createSocket = function () {
var self = this;
var createSocketFunc = exports.createSocket || function () {
var protocol = 'ws:';
if (window.location.protocol === 'https:') protocol = 'wss:';
var defaultPath = window.location.pathname;
// some older WebKit browsers return the pathname already decoded;
// if we find invalid URL characters in the path, encode them
if (!/^([$#!&-;=?-[\]_a-z~]|%[0-9a-fA-F]{2})+$/.test(defaultPath)) {
defaultPath = encodeURI(defaultPath);
// Bizarrely, QtWebKit requires us to encode these characters *twice*
if (browser.isQt) {
defaultPath = encodeURI(defaultPath);
}
}
if (!/\/$/.test(defaultPath)) defaultPath += '/';
defaultPath += 'websocket/';
var ws = new WebSocket(protocol + '//' + window.location.host + defaultPath);
ws.binaryType = 'arraybuffer';
return ws;
};
var socket = createSocketFunc();
var hasOpened = false;
socket.onopen = function () {
hasOpened = true;
$(document).trigger({
type: 'shiny:connected',
socket: socket
});
self.onConnected();
socket.send(JSON.stringify({
method: 'init',
data: self.$initialInput
}));
while (self.$pendingMessages.length) {
var msg = self.$pendingMessages.shift();
socket.send(msg);
}
};
socket.onmessage = function (e) {
self.dispatchMessage(e.data);
};
// Called when a successfully-opened websocket is closed, or when an
// attempt to open a connection fails.
socket.onclose = function () {
// These things are needed only if we've successfully opened the
// websocket.
if (hasOpened) {
$(document).trigger({
type: 'shiny:disconnected',
socket: socket
});
self.$notifyDisconnected();
}
self.onDisconnected(); // Must be run before self.$removeSocket()
self.$removeSocket();
};
return socket;
};
this.sendInput = function (values) {
var msg = JSON.stringify({
method: 'update',
data: values
});
this.$sendMsg(msg);
$.extend(this.$inputValues, values);
this.$updateConditionals();
};
this.$notifyDisconnected = function () {
// function to normalize hostnames
var normalize = function normalize(hostname) {
if (hostname == "127.0.0.1") return "localhost";else return hostname;
};
// Send a 'disconnected' message to parent if we are on the same domin
var parentUrl = parent !== window ? document.referrer : null;
if (parentUrl) {
// parse the parent href
var a = document.createElement('a');
a.href = parentUrl;
// post the disconnected message if the hostnames are the same
if (normalize(a.hostname) == normalize(window.location.hostname)) {
var protocol = a.protocol.replace(':', ''); // browser compatability
var origin = protocol + '://' + a.hostname;
if (a.port) origin = origin + ':' + a.port;
parent.postMessage('disconnected', origin);
}
}
};
this.$removeSocket = function () {
this.$socket = null;
};
this.$scheduleReconnect = function (delay) {
var self = this;
scheduledReconnect = setTimeout(function () {
self.reconnect();
}, delay);
};
// How long should we wait before trying the next reconnection?
// The delay will increase with subsequent attempts.
// .next: Return the time to wait for next connection, and increment counter.
// .reset: Reset the attempt counter.
var reconnectDelay = function () {
var attempts = 0;
// Time to wait before each reconnection attempt. If we go through all of
// these values, repeated use the last one. Add 500ms to each one so that
// in the last 0.5s, it shows "..."
var delays = [1500, 1500, 2500, 2500, 5500, 5500, 10500];
return {
next: function next() {
var i = attempts;
// Instead of going off the end, use the last one
if (i >= delays.length) {
i = delays.length - 1;
}
attempts++;
return delays[i];
},
reset: function reset() {
attempts = 0;
}
};
}();
this.onDisconnected = function () {
// Add gray-out overlay, if not already present
var $overlay = $('#shiny-disconnected-overlay');
if ($overlay.length === 0) {
$(document.body).append('
');
}
// To try a reconnect, both the app (this.$allowReconnect) and the
// server (this.$socket.allowReconnect) must allow reconnections, or
// session$allowReconnect("force") was called. The "force" option should
// only be used for testing.
if (this.$allowReconnect === true && this.$socket.allowReconnect === true || this.$allowReconnect === "force") {
var delay = reconnectDelay.next();
exports.showReconnectDialog(delay);
this.$scheduleReconnect(delay);
}
};
this.onConnected = function () {
$('#shiny-disconnected-overlay').remove();
exports.hideReconnectDialog();
reconnectDelay.reset();
};
// NB: Including blobs will cause IE to break!
// TODO: Make blobs work with Internet Explorer
//
// Websocket messages are normally one-way--i.e. the client passes a
// message to the server but there is no way for the server to provide
// a response to that specific message. makeRequest provides a way to
// do asynchronous RPC over websocket. Each request has a method name
// and arguments, plus optionally one or more binary blobs can be
// included as well. The request is tagged with a unique number that
// the server will use to label the corresponding response.
//
// @param method A string that tells the server what logic to run.
// @param args An array of objects that should also be passed to the
// server in JSON-ified form.
// @param onSuccess A function that will be called back if the server
// responds with success. If the server provides a value in the
// response, the function will be called with it as the only argument.
// @param onError A function that will be called back if the server
// responds with error, or if the request fails for any other reason.
// The parameter to onError will be a string describing the error.
// @param blobs Optionally, an array of Blob, ArrayBuffer, or string
// objects that will be made available to the server as part of the
// request. Strings will be encoded using UTF-8.
this.makeRequest = function (method, args, onSuccess, onError, blobs) {
var requestId = this.$nextRequestId;
while (this.$activeRequests[requestId]) {
requestId = (requestId + 1) % 1000000000;
}
this.$nextRequestId = requestId + 1;
this.$activeRequests[requestId] = {
onSuccess: onSuccess,
onError: onError
};
var msg = JSON.stringify({
method: method,
args: args,
tag: requestId
});
if (blobs) {
// We have binary data to transfer; form a different kind of packet.
// Start with a 4-byte signature, then for each blob, emit 4 bytes for
// the length followed by the blob. The json payload is UTF-8 encoded
// and used as the first blob.
var uint32_to_buf = function uint32_to_buf(val) {
var buffer = new ArrayBuffer(4);
var view = new DataView(buffer);
view.setUint32(0, val, true); // little-endian
return buffer;
};
var payload = [];
payload.push(uint32_to_buf(0x01020202)); // signature
var jsonBuf = makeBlob([msg]);
payload.push(uint32_to_buf(jsonBuf.size));
payload.push(jsonBuf);
for (var i = 0; i < blobs.length; i++) {
payload.push(uint32_to_buf(blobs[i].byteLength || blobs[i].size || 0));
payload.push(blobs[i]);
}
msg = makeBlob(payload);
}
this.$sendMsg(msg);
};
this.$sendMsg = function (msg) {
if (!this.$socket.readyState) {
this.$pendingMessages.push(msg);
} else {
this.$socket.send(msg);
}
};
this.receiveError = function (name, error) {
if (this.$errors[name] === error) return;
this.$errors[name] = error;
delete this.$values[name];
var binding = this.$bindings[name];
var evt = jQuery.Event('shiny:error');
evt.name = name;
evt.error = error;
evt.binding = binding;
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding && binding.onValueError) {
binding.onValueError(evt.error);
}
};
this.receiveOutput = function (name, value) {
if (this.$values[name] === value) return undefined;
this.$values[name] = value;
delete this.$errors[name];
var binding = this.$bindings[name];
var evt = jQuery.Event('shiny:value');
evt.name = name;
evt.value = value;
evt.binding = binding;
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding) {
binding.onValueChange(evt.value);
}
return value;
};
this.bindOutput = function (id, binding) {
if (!id) throw "Can't bind an element with no ID";
if (this.$bindings[id]) throw "Duplicate binding for ID " + id;
this.$bindings[id] = binding;
if (this.$values[id] !== undefined) binding.onValueChange(this.$values[id]);else if (this.$errors[id] !== undefined) binding.onValueError(this.$errors[id]);
return binding;
};
this.unbindOutput = function (id, binding) {
if (this.$bindings[id] === binding) {
delete this.$bindings[id];
return true;
} else {
return false;
}
};
this.$updateConditionals = function () {
$(document).trigger({
type: 'shiny:conditional'
});
var inputs = {};
// Input keys use "name:type" format; we don't want the user to
// have to know about the type suffix when referring to inputs.
for (var name in this.$inputValues) {
if (this.$inputValues.hasOwnProperty(name)) {
var shortName = name.replace(/:.*/, '');
inputs[shortName] = this.$inputValues[name];
}
}
var scope = { input: inputs, output: this.$values };
var conditionals = $(document).find('[data-display-if]');
for (var i = 0; i < conditionals.length; i++) {
var el = $(conditionals[i]);
var condFunc = el.data('data-display-if-func');
if (!condFunc) {
var condExpr = el.attr('data-display-if');
condFunc = scopeExprToFunc(condExpr);
el.data('data-display-if-func', condFunc);
}
var show = condFunc(scope);
var showing = el.css("display") !== "none";
if (show !== showing) {
if (show) {
el.trigger('show');
el.show();
el.trigger('shown');
} else {
el.trigger('hide');
el.hide();
el.trigger('hidden');
}
}
}
};
// Message handler management functions =================================
// Records insertion order of handlers. Maps number to name. This is so
// we can dispatch messages to handlers in the order that handlers were
// added.
var messageHandlerOrder = [];
// Keep track of handlers by name. Maps name to handler function.
var messageHandlers = {};
// Two categories of message handlers: those that are from Shiny, and those
// that are added by the user. The Shiny ones handle messages in
// msgObj.values, msgObj.errors, and so on. The user ones handle messages
// in msgObj.custom.foo and msgObj.custom.bar.
var customMessageHandlerOrder = [];
var customMessageHandlers = {};
// Adds Shiny (internal) message handler
function addMessageHandler(type, handler) {
if (messageHandlers[type]) {
throw 'handler for message of type "' + type + '" already added.';
}
if (typeof handler !== 'function') {
throw 'handler must be a function.';
}
if (handler.length !== 1) {
throw 'handler must be a function that takes one argument.';
}
messageHandlerOrder.push(type);
messageHandlers[type] = handler;
}
// Adds custom message handler - this one is exposed to the user
function addCustomMessageHandler(type, handler) {
if (customMessageHandlers[type]) {
throw 'handler for message of type "' + type + '" already added.';
}
if (typeof handler !== 'function') {
throw 'handler must be a function.';
}
if (handler.length !== 1) {
throw 'handler must be a function that takes one argument.';
}
customMessageHandlerOrder.push(type);
customMessageHandlers[type] = handler;
}
exports.addCustomMessageHandler = addCustomMessageHandler;
this.dispatchMessage = function (msg) {
var msgObj = JSON.parse(msg);
var evt = jQuery.Event('shiny:message');
evt.message = msgObj;
$(document).trigger(evt);
if (evt.isDefaultPrevented()) return;
// Send msgObj.foo and msgObj.bar to appropriate handlers
this._sendMessagesToHandlers(evt.message, messageHandlers, messageHandlerOrder);
this.$updateConditionals();
};
// A function for sending messages to the appropriate handlers.
// - msgObj: the object containing messages, with format {msgObj.foo, msObj.bar
this._sendMessagesToHandlers = function (msgObj, handlers, handlerOrder) {
// Dispatch messages to handlers, if handler is present
for (var i = 0; i < handlerOrder.length; i++) {
var msgType = handlerOrder[i];
if (msgObj.hasOwnProperty(msgType)) {
// Execute each handler with 'this' referring to the present value of
// 'this'
handlers[msgType].call(this, msgObj[msgType]);
}
}
};
// Message handlers =====================================================
addMessageHandler('values', function (message) {
for (var name in this.$bindings) {
if (this.$bindings.hasOwnProperty(name)) this.$bindings[name].showProgress(false);
}
for (var key in message) {
if (message.hasOwnProperty(key)) this.receiveOutput(key, message[key]);
}
});
addMessageHandler('errors', function (message) {
for (var key in message) {
if (message.hasOwnProperty(key)) this.receiveError(key, message[key]);
}
});
addMessageHandler('inputMessages', function (message) {
// inputMessages should be an array
for (var i = 0; i < message.length; i++) {
var $obj = $('.shiny-bound-input#' + $escape(message[i].id));
var inputBinding = $obj.data('shiny-input-binding');
// Dispatch the message to the appropriate input object
if ($obj.length > 0) {
var el = $obj[0];
var evt = jQuery.Event('shiny:updateinput');
evt.message = message[i].message;
evt.binding = inputBinding;
$(el).trigger(evt);
if (!evt.isDefaultPrevented()) inputBinding.receiveMessage(el, evt.message);
}
}
});
addMessageHandler('javascript', function (message) {
/*jshint evil: true */
eval(message);
});
addMessageHandler('console', function (message) {
for (var i = 0; i < message.length; i++) {
if (console.log) console.log(message[i]);
}
});
addMessageHandler('progress', function (message) {
if (message.type && message.message) {
var handler = progressHandlers[message.type];
if (handler) handler.call(this, message.message);
}
});
addMessageHandler('notification', function (message) {
if (message.type === 'show') exports.notifications.show(message.message);else if (message.type === 'remove') exports.notifications.remove(message.message);else throw 'Unkown notification type: ' + message.type;
});
addMessageHandler('modal', function (message) {
if (message.type === 'show') exports.modal.show(message.message);else if (message.type === 'remove') exports.modal.remove(); // For 'remove', message content isn't used
else throw 'Unkown modal type: ' + message.type;
});
addMessageHandler('response', function (message) {
var requestId = message.tag;
var request = this.$activeRequests[requestId];
if (request) {
delete this.$activeRequests[requestId];
if ('value' in message) request.onSuccess(message.value);else request.onError(message.error);
}
});
addMessageHandler('allowReconnect', function (message) {
if (message === true || message === false || message === "force") {
this.$allowReconnect = message;
} else {
throw "Invalid value for allowReconnect: " + message;
}
});
addMessageHandler('custom', function (message) {
// For old-style custom messages - should deprecate and migrate to new
// method
if (exports.oncustommessage) {
exports.oncustommessage(message);
}
// Send messages.foo and messages.bar to appropriate handlers
this._sendMessagesToHandlers(message, customMessageHandlers, customMessageHandlerOrder);
});
addMessageHandler('config', function (message) {
this.config = message;
});
addMessageHandler('busy', function (message) {
if (message === 'busy') {
$(document.documentElement).addClass('shiny-busy');
$(document).trigger('shiny:busy');
} else if (message === 'idle') {
$(document.documentElement).removeClass('shiny-busy');
$(document).trigger('shiny:idle');
}
});
addMessageHandler('recalculating', function (message) {
if (message.hasOwnProperty('name') && message.hasOwnProperty('status')) {
var binding = this.$bindings[message.name];
$(binding ? binding.el : null).trigger({
type: 'shiny:' + message.status
});
}
});
addMessageHandler('reload', function (message) {
window.location.reload();
});
addMessageHandler('shiny-insert-ui', function (message) {
var targets = $(message.selector);
if (targets.length === 0) {
// render the HTML and deps to a null target, so
// the side-effect of rendering the deps, singletons,
// and still occur
exports.renderHtml($([]), message.content.html, message.content.deps);
} else {
targets.each(function (i, target) {
exports.renderContent(target, message.content, message.where);
return message.multiple;
});
}
});
addMessageHandler('shiny-remove-ui', function (message) {
var els = $(message.selector);
els.each(function (i, el) {
exports.unbindAll(el, true);
$(el).remove();
// If `multiple` is false, returning false terminates the function
// and no other elements are removed; if `multiple` is true,
// returning true continues removing all remaining elements.
return message.multiple;
});
});
addMessageHandler('updateQueryString', function (message) {
window.history.replaceState(null, null, message.queryString);
});
addMessageHandler("resetBrush", function (message) {
exports.resetBrush(message.brushId);
});
// Progress reporting ====================================================
var progressHandlers = {
// Progress for a particular object
binding: function binding(message) {
var key = message.id;
var binding = this.$bindings[key];
if (binding && binding.showProgress) {
binding.showProgress(true);
}
},
// Open a page-level progress bar
open: function open(message) {
// Progress bar starts hidden; will be made visible if a value is provided
// during updates.
exports.notifications.show({
html: '
' + '
' + '
' + 'message ' + '' + '
' + '
',
id: message.id,
duration: null
});
},
// Update page-level progress bar
update: function update(message) {
var $progress = $('#shiny-progress-' + message.id);
if ($progress.length === 0) return;
if (typeof message.message !== 'undefined') {
$progress.find('.progress-message').text(message.message);
}
if (typeof message.detail !== 'undefined') {
$progress.find('.progress-detail').text(message.detail);
}
if (typeof message.value !== 'undefined') {
if (message.value !== null) {
$progress.find('.progress').show();
$progress.find('.progress-bar').width(message.value * 100 + '%');
} else {
$progress.find('.progress').hide();
}
}
},
// Close page-level progress bar
close: function close(message) {
exports.notifications.remove(message.id);
}
};
exports.progressHandlers = progressHandlers;
}).call(ShinyApp.prototype);
exports.showReconnectDialog = function () {
var reconnectTime = null;
function updateTime() {
var $time = $("#shiny-reconnect-time");
// If the time has been removed, exit and don't reschedule this function.
if ($time.length === 0) return;
var seconds = Math.floor((reconnectTime - new Date().getTime()) / 1000);
if (seconds > 0) {
$time.text(" in " + seconds + "s");
} else {
$time.text("...");
}
// Reschedule this function after 1 second
setTimeout(updateTime, 1000);
}
return function (delay) {
reconnectTime = new Date().getTime() + delay;
// If there's already a reconnect dialog, don't add another
if ($('#shiny-reconnect-text').length > 0) return;
var html = 'Attempting to reconnect' + '';
var action = 'Try now';
exports.notifications.show({
id: "reconnect",
html: html,
action: action,
duration: null,
closeButton: false,
type: 'warning'
});
updateTime();
};
}();
exports.hideReconnectDialog = function () {
exports.notifications.remove("reconnect");
};
//---------------------------------------------------------------------
// Source file: ../srcjs/notifications.js
exports.notifications = function () {
// Milliseconds to fade in or out
var fadeDuration = 250;
function show() {
var _ref = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var _ref$html = _ref.html;
var html = _ref$html === undefined ? '' : _ref$html;
var _ref$action = _ref.action;
var action = _ref$action === undefined ? '' : _ref$action;
var _ref$deps = _ref.deps;
var deps = _ref$deps === undefined ? [] : _ref$deps;
var _ref$duration = _ref.duration;
var duration = _ref$duration === undefined ? 5000 : _ref$duration;
var _ref$id = _ref.id;
var id = _ref$id === undefined ? null : _ref$id;
var _ref$closeButton = _ref.closeButton;
var closeButton = _ref$closeButton === undefined ? true : _ref$closeButton;
var _ref$type = _ref.type;
var type = _ref$type === undefined ? null : _ref$type;
if (!id) id = randomId();
// Create panel if necessary
_createPanel();
// Get existing DOM element for this ID, or create if needed.
var $notification = _get(id);
if ($notification.length === 0) $notification = _create(id);
// Render html and dependencies
var newHtml = '
' + html + '
' + ('
' + action + '
');
var $content = $notification.find('.shiny-notification-content');
exports.renderContent($content, { html: newHtml, deps: deps });
// Remove any existing classes of the form 'shiny-notification-xxxx'.
// The xxxx would be strings like 'warning'.
var classes = $notification.attr('class').split(/\s+/).filter(function (cls) {
return cls.match(/^shiny-notification-/);
}).join(' ');
$notification.removeClass(classes);
// Add class. 'default' means no additional CSS class.
if (type && type !== 'default') $notification.addClass('shiny-notification-' + type);
// Make sure that the presence/absence of close button matches with value
// of `closeButton`.
var $close = $notification.find('.shiny-notification-close');
if (closeButton && $close.length === 0) {
$notification.append('
×
');
} else if (!closeButton && $close.length !== 0) {
$close.remove();
}
// If duration was provided, schedule removal. If not, clear existing
// removal callback (this happens if a message was first added with
// a duration, and then updated with no duration).
if (duration) _addRemovalCallback(id, duration);else _clearRemovalCallback(id);
return id;
}
function remove(id) {
_get(id).fadeOut(fadeDuration, function () {
exports.unbindAll(this);
this.remove();
// If no more notifications, remove the panel from the DOM.
if (_ids().length === 0) {
_getPanel().remove();
}
});
}
// Returns an individual notification DOM object (wrapped in jQuery).
function _get(id) {
if (!id) return null;
return _getPanel().find('#shiny-notification-' + $escape(id));
}
// Return array of all notification IDs
function _ids() {
return _getPanel().find('.shiny-notification').map(function () {
return this.id.replace(/shiny-notification-/, '');
}).get();
}
// Returns the notification panel DOM object (wrapped in jQuery).
function _getPanel() {
return $('#shiny-notification-panel');
}
// Create notifications panel and return the jQuery object. If the DOM
// element already exists, just return it.
function _createPanel() {
var $panel = _getPanel();
if ($panel.length > 0) return $panel;
$('body').append('
');
return $panel;
}
// Create a notification DOM element and return the jQuery object. If the
// DOM element already exists for the ID, just return it without creating.
function _create(id) {
var $notification = _get(id);
if ($notification.length === 0) {
$notification = $('
' + '
×
' + '' + '
');
$notification.find('.shiny-notification-close').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
remove(id);
});
_getPanel().append($notification);
}
return $notification;
}
// Add a callback to remove a notification after a delay in ms.
function _addRemovalCallback(id, delay) {
// If there's an existing removalCallback, clear it before adding the new
// one.
_clearRemovalCallback(id);
// Attach new removal callback
var removalCallback = setTimeout(function () {
remove(id);
}, delay);
_get(id).data('removalCallback', removalCallback);
}
// Clear a removal callback from a notification, if present.
function _clearRemovalCallback(id) {
var $notification = _get(id);
var oldRemovalCallback = $notification.data('removalCallback');
if (oldRemovalCallback) {
clearTimeout(oldRemovalCallback);
}
}
return {
show: show,
remove: remove
};
}();
//---------------------------------------------------------------------
// Source file: ../srcjs/modal.js
exports.modal = {
// Show a modal dialog. This is meant to handle two types of cases: one is
// that the content is a Bootstrap modal dialog, and the other is that the
// content is non-Bootstrap. Bootstrap modals require some special handling,
// which is coded in here.
show: function show() {
var _ref2 = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var _ref2$html = _ref2.html;
var html = _ref2$html === undefined ? '' : _ref2$html;
var _ref2$deps = _ref2.deps;
var deps = _ref2$deps === undefined ? [] : _ref2$deps;
// If there was an existing Bootstrap modal, then there will be a modal-
// backdrop div that was added outside of the modal wrapper, and it must be
// removed; otherwise there can be multiple of these divs.
$('.modal-backdrop').remove();
// Get existing wrapper DOM element, or create if needed.
var $modal = $('#shiny-modal-wrapper');
if ($modal.length === 0) {
$modal = $('');
$('body').append($modal);
// If the wrapper's content is a Bootstrap modal, then when the inner
// modal is hidden, remove the entire thing, including wrapper.
$modal.on('hidden.bs.modal', function () {
exports.unbindAll($modal);
$modal.remove();
});
}
// Set/replace contents of wrapper with html.
exports.renderContent($modal, { html: html, deps: deps });
},
remove: function remove() {
var $modal = $('#shiny-modal-wrapper');
// Look for a Bootstrap modal and if present, trigger hide event. This will
// trigger the hidden.bs.modal callback that we set in show(), which unbinds
// and removes the element.
if ($modal.find('.modal').length > 0) {
$modal.find('.modal').modal('hide');
} else {
// If not a Bootstrap modal dialog, simply unbind and remove it.
exports.unbindAll($modal);
$modal.remove();
}
}
};
//---------------------------------------------------------------------
// Source file: ../srcjs/file_processor.js
// Generic driver class for doing chunk-wise asynchronous processing of a
// FileList object. Subclass/clone it and override the `on*` functions to
// make it do something useful.
var FileProcessor = function FileProcessor(files) {
this.files = files;
this.fileIndex = -1;
// Currently need to use small chunk size because R-Websockets can't
// handle continuation frames
this.aborted = false;
this.completed = false;
// TODO: Register error/abort callbacks
this.$run();
};
(function () {
// Begin callbacks. Subclassers/cloners may override any or all of these.
this.onBegin = function (files, cont) {
setTimeout(cont, 0);
};
this.onFile = function (file, cont) {
setTimeout(cont, 0);
};
this.onComplete = function () {};
this.onAbort = function () {};
// End callbacks
// Aborts processing, unless it's already completed
this.abort = function () {
if (this.completed || this.aborted) return;
this.aborted = true;
this.onAbort();
};
// Returns a bound function that will call this.$run one time.
this.$getRun = function () {
var self = this;
var called = false;
return function () {
if (called) return;
called = true;
self.$run();
};
};
// This function will be called multiple times to advance the process.
// It relies on the state of the object's fields to know what to do next.
this.$run = function () {
if (this.aborted || this.completed) return;
if (this.fileIndex < 0) {
// Haven't started yet--begin
this.fileIndex = 0;
this.onBegin(this.files, this.$getRun());
return;
}
if (this.fileIndex === this.files.length) {
// Just ended
this.completed = true;
this.onComplete();
return;
}
// If we got here, then we have a file to process, or we are
// in the middle of processing a file, or have just finished
// processing a file.
var file = this.files[this.fileIndex++];
this.onFile(file, this.$getRun());
};
}).call(FileProcessor.prototype);
//---------------------------------------------------------------------
// Source file: ../srcjs/binding_registry.js
var BindingRegistry = function BindingRegistry() {
this.bindings = [];
this.bindingNames = {};
};
(function () {
this.register = function (binding, bindingName, priority) {
var bindingObj = { binding: binding, priority: priority || 0 };
this.bindings.unshift(bindingObj);
if (bindingName) {
this.bindingNames[bindingName] = bindingObj;
binding.name = bindingName;
}
};
this.setPriority = function (bindingName, priority) {
var bindingObj = this.bindingNames[bindingName];
if (!bindingObj) throw "Tried to set priority on unknown binding " + bindingName;
bindingObj.priority = priority || 0;
};
this.getPriority = function (bindingName) {
var bindingObj = this.bindingNames[bindingName];
if (!bindingObj) return false;
return bindingObj.priority;
};
this.getBindings = function () {
// Sort the bindings. The ones with higher priority are consulted
// first; ties are broken by most-recently-registered.
return mergeSort(this.bindings, function (a, b) {
return b.priority - a.priority;
});
};
}).call(BindingRegistry.prototype);
var inputBindings = exports.inputBindings = new BindingRegistry();
var outputBindings = exports.outputBindings = new BindingRegistry();
//---------------------------------------------------------------------
// Source file: ../srcjs/output_binding.js
var OutputBinding = exports.OutputBinding = function () {};
(function () {
// Returns a jQuery object or element array that contains the
// descendants of scope that match this binding
this.find = function (scope) {
throw "Not implemented";
};
this.getId = function (el) {
return el['data-input-id'] || el.id;
};
this.onValueChange = function (el, data) {
this.clearError(el);
this.renderValue(el, data);
};
this.onValueError = function (el, err) {
this.renderError(el, err);
};
this.renderError = function (el, err) {
this.clearError(el);
if (err.message === '') {
// not really error, but we just need to wait (e.g. action buttons)
$(el).empty();
return;
}
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(' ');
}
$(el).addClass(errClass).text(err.message);
};
this.clearError = function (el) {
$(el).attr('class', function (i, c) {
return c.replace(/(^|\s)shiny-output-error\S*/g, '');
});
};
this.showProgress = function (el, show) {
var RECALC_CLASS = 'recalculating';
if (show) $(el).addClass(RECALC_CLASS);else $(el).removeClass(RECALC_CLASS);
};
}).call(OutputBinding.prototype);
//---------------------------------------------------------------------
// Source file: ../srcjs/output_binding_text.js
var textOutputBinding = new OutputBinding();
$.extend(textOutputBinding, {
find: function find(scope) {
return $(scope).find('.shiny-text-output');
},
renderValue: function renderValue(el, data) {
$(el).text(data);
}
});
outputBindings.register(textOutputBinding, 'shiny.textOutput');
//---------------------------------------------------------------------
// Source file: ../srcjs/output_binding_image.js
var imageOutputBinding = new OutputBinding();
$.extend(imageOutputBinding, {
find: function find(scope) {
return $(scope).find('.shiny-image-output, .shiny-plot-output');
},
renderValue: function renderValue(el, data) {
// The overall strategy:
// * Clear out existing image and event handlers.
// * Create new image.
// * Create various event handlers.
// * Bind those event handlers to events.
// * Insert the new image.
var outputId = this.getId(el);
var $el = $(el);
var img;
// Remove event handlers that were added in previous renderValue()
$el.off('.image_output');
// Get existing img element if present.
var $img = $el.find('img');
if ($img.length === 0) {
// If a img element is not already present, that means this is either
// the first time renderValue() has been called, or this is after an
// error.
img = document.createElement('img');
$el.append(img);
$img = $(img);
} else {
// Trigger custom 'reset' event for any existing images in the div
img = $img[0];
$img.trigger('reset');
}
if (!data) {
$el.empty();
return;
}
// If value is undefined, return alternate. Sort of like ||, except it won't
// return alternate for other falsy values (0, false, null).
function OR(value, alternate) {
if (value === undefined) return alternate;
return value;
}
var opts = {
clickId: $el.data('click-id'),
clickClip: OR(strToBool($el.data('click-clip')), true),
dblclickId: $el.data('dblclick-id'),
dblclickClip: OR(strToBool($el.data('dblclick-clip')), true),
dblclickDelay: OR($el.data('dblclick-delay'), 400),
hoverId: $el.data('hover-id'),
hoverClip: OR(strToBool($el.data('hover-clip')), true),
hoverDelayType: OR($el.data('hover-delay-type'), 'debounce'),
hoverDelay: OR($el.data('hover-delay'), 300),
hoverNullOutside: OR(strToBool($el.data('hover-null-outside')), false),
brushId: $el.data('brush-id'),
brushClip: OR(strToBool($el.data('brush-clip')), true),
brushDelayType: OR($el.data('brush-delay-type'), 'debounce'),
brushDelay: OR($el.data('brush-delay'), 300),
brushFill: OR($el.data('brush-fill'), '#666'),
brushStroke: OR($el.data('brush-stroke'), '#000'),
brushOpacity: OR($el.data('brush-opacity'), 0.3),
brushDirection: OR($el.data('brush-direction'), 'xy'),
brushResetOnNew: OR(strToBool($el.data('brush-reset-on-new')), false),
coordmap: data.coordmap
};
// Copy items from data to img. Don't set the coordmap as an attribute.
$.each(data, function (key, value) {
if (value === null || key === 'coordmap') {
return;
}
img.setAttribute(key, value);
});
// Unset any attributes in the current img that were not provided in the
// new data.
for (var i = 0; i < img.attributes.length; i++) {
var attrib = img.attributes[i];
// Need to check attrib.specified on IE because img.attributes contains
// all possible attributes on IE.
if (attrib.specified && !data.hasOwnProperty(attrib.name)) {
img.removeAttribute(attrib.name);
}
}
if (!opts.coordmap) opts.coordmap = [];
imageutils.initCoordmap($el, opts.coordmap);
// This object listens for mousedowns, and triggers mousedown2 and dblclick2
// events as appropriate.
var clickInfo = imageutils.createClickInfo($el, opts.dblclickId, opts.dblclickDelay);
$el.on('mousedown.image_output', clickInfo.mousedown);
if (browser.isIE && browser.IEVersion === 8) {
$el.on('dblclick.image_output', clickInfo.dblclickIE8);
}
// ----------------------------------------------------------
// Register the various event handlers
// ----------------------------------------------------------
if (opts.clickId) {
var clickHandler = imageutils.createClickHandler(opts.clickId, opts.clickClip, opts.coordmap);
$el.on('mousedown2.image_output', clickHandler.mousedown);
// When img is reset, do housekeeping: clear $el's mouse listener and
// call the handler's onResetImg callback.
$img.on('reset', clickHandler.onResetImg);
}
if (opts.dblclickId) {
// We'll use the clickHandler's mousedown function, but register it to
// our custom 'dblclick2' event.
var dblclickHandler = imageutils.createClickHandler(opts.dblclickId, opts.clickClip, opts.coordmap);
$el.on('dblclick2.image_output', dblclickHandler.mousedown);
$img.on('reset', dblclickHandler.onResetImg);
}
if (opts.hoverId) {
var hoverHandler = imageutils.createHoverHandler(opts.hoverId, opts.hoverDelay, opts.hoverDelayType, opts.hoverClip, opts.hoverNullOutside, opts.coordmap);
$el.on('mousemove.image_output', hoverHandler.mousemove);
$el.on('mouseout.image_output', hoverHandler.mouseout);
$img.on('reset', hoverHandler.onResetImg);
}
if (opts.brushId) {
// Make image non-draggable (Chrome, Safari)
$img.css('-webkit-user-drag', 'none');
// Firefox, IE<=10
$img.on('dragstart', function () {
return false;
});
// Disable selection of image and text when dragging in IE<=10
$el.on('selectstart.image_output', function () {
return false;
});
var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts, opts.coordmap, outputId);
$el.on('mousedown.image_output', brushHandler.mousedown);
$el.on('mousemove.image_output', brushHandler.mousemove);
$img.on('reset', brushHandler.onResetImg);
}
if (opts.clickId || opts.dblclickId || opts.hoverId || opts.brushId) {
$el.addClass('crosshair');
}
if (data.error) console.log('Error on server extracting coordmap: ' + data.error);
},
renderError: function renderError(el, err) {
$(el).find('img').trigger('reset');
OutputBinding.prototype.renderError.call(this, el, err);
},
clearError: function clearError(el) {
// Remove all elements except img and the brush; this is usually just
// error messages.
$(el).contents().filter(function () {
return this.tagName !== "IMG" && this.id !== el.id + '_brush';
}).remove();
OutputBinding.prototype.clearError.call(this, el);
}
});
outputBindings.register(imageOutputBinding, 'shiny.imageOutput');
var imageutils = {};
// Modifies the panel objects in a coordmap, adding scale(), scaleInv(),
// and clip() functions to each one.
imageutils.initPanelScales = function (coordmap) {
// Map a value x from a domain to a range. If clip is true, clip it to the
// range.
function mapLinear(x, domainMin, domainMax, rangeMin, rangeMax, clip) {
// By default, clip to range
clip = clip || true;
var factor = (rangeMax - rangeMin) / (domainMax - domainMin);
var val = x - domainMin;
var newval = val * factor + rangeMin;
if (clip) {
var max = Math.max(rangeMax, rangeMin);
var min = Math.min(rangeMax, rangeMin);
if (newval > max) newval = max;else if (newval < min) newval = min;
}
return newval;
}
// Create scale and inverse-scale functions for a single direction (x or y).
function scaler1D(domainMin, domainMax, rangeMin, rangeMax, logbase) {
return {
scale: function scale(val, clip) {
if (logbase) val = Math.log(val) / Math.log(logbase);
return mapLinear(val, domainMin, domainMax, rangeMin, rangeMax, clip);
},
scaleInv: function scaleInv(val, clip) {
var res = mapLinear(val, rangeMin, rangeMax, domainMin, domainMax, clip);
if (logbase) res = Math.pow(logbase, res);
return res;
}
};
}
// Modify panel, adding scale and inverse-scale functions that take objects
// like {x:1, y:3}, and also add clip function.
function addScaleFuns(panel) {
var d = panel.domain;
var r = panel.range;
var xlog = panel.log && panel.log.x ? panel.log.x : null;
var ylog = panel.log && panel.log.y ? panel.log.y : null;
var xscaler = scaler1D(d.left, d.right, r.left, r.right, xlog);
var yscaler = scaler1D(d.bottom, d.top, r.bottom, r.top, ylog);
panel.scale = function (val, clip) {
return {
x: xscaler.scale(val.x, clip),
y: yscaler.scale(val.y, clip)
};
};
panel.scaleInv = function (val, clip) {
return {
x: xscaler.scaleInv(val.x, clip),
y: yscaler.scaleInv(val.y, clip)
};
};
// Given a scaled offset (in pixels), clip it to the nearest panel region.
panel.clip = function (offset) {
var newOffset = {
x: offset.x,
y: offset.y
};
var bounds = panel.range;
if (offset.x > bounds.right) newOffset.x = bounds.right;else if (offset.x < bounds.left) newOffset.x = bounds.left;
if (offset.y > bounds.bottom) newOffset.y = bounds.bottom;else if (offset.y < bounds.top) newOffset.y = bounds.top;
return newOffset;
};
}
// Add the functions to each panel object.
for (var i = 0; i < coordmap.length; i++) {
var panel = coordmap[i];
addScaleFuns(panel);
}
};
// This adds functions to the coordmap object to handle various
// coordinate-mapping tasks, and send information to the server.
// The input coordmap is an array of objects, each of which represents a panel.
// coordmap must be an array, even if empty, so that it can be modified in
// place; when empty, we add a dummy panel to the array.
// It also calls initPanelScales, which modifies each panel object to have
// scale, scaleInv, and clip functions.
imageutils.initCoordmap = function ($el, coordmap) {
var el = $el[0];
// If we didn't get any panels, create a dummy one where the domain and range
// are simply the pixel dimensions.
// that we modify.
if (coordmap.length === 0) {
var bounds = {
top: 0,
left: 0,
right: el.clientWidth - 1,
bottom: el.clientHeight - 1
};
coordmap[0] = {
domain: bounds,
range: bounds,
mapping: {}
};
}
// Add scaling functions to each panel
imageutils.initPanelScales(coordmap);
// Firefox doesn't have offsetX/Y, so we need to use an alternate
// method of calculation for it. Even though other browsers do have
// offsetX/Y, we need to calculate relative to $el, because sometimes the
// mouse event can come with offset relative to other elements on the
// page. This happens when the event listener is bound to, say, window.
coordmap.mouseOffset = function (mouseEvent) {
var offset = $el.offset();
return {
x: mouseEvent.pageX - offset.left,
y: mouseEvent.pageY - offset.top
};
};
// Given two sets of x/y coordinates, return an object representing the
// min and max x and y values. (This could be generalized to any number
// of points).
coordmap.findBox = function (offset1, offset2) {
return {
xmin: Math.min(offset1.x, offset2.x),
xmax: Math.max(offset1.x, offset2.x),
ymin: Math.min(offset1.y, offset2.y),
ymax: Math.max(offset1.y, offset2.y)
};
};
// Shift an array of values so that they are within a min and max.
// The vals will be shifted so that they maintain the same spacing
// internally. If the range in vals is larger than the range of
// min and max, the result might not make sense.
coordmap.shiftToRange = function (vals, min, max) {
if (!(vals instanceof Array)) vals = [vals];
var maxval = Math.max.apply(null, vals);
var minval = Math.min.apply(null, vals);
var shiftAmount = 0;
if (maxval > max) {
shiftAmount = max - maxval;
} else if (minval < min) {
shiftAmount = min - minval;
}
var newvals = [];
for (var i = 0; i < vals.length; i++) {
newvals[i] = vals[i] + shiftAmount;
}
return newvals;
};
// Given an offset, return an object representing which panel it's in. The
// `expand` argument tells it to expand the panel area by that many pixels.
// It's possible for an offset to be within more than one panel, because
// of the `expand` value. If that's the case, find the nearest panel.
coordmap.getPanel = function (offset, expand) {
expand = expand || 0;
var x = offset.x;
var y = offset.y;
var matches = []; // Panels that match
var dists = []; // Distance of offset to each matching panel
var b;
for (var i = 0; i < coordmap.length; i++) {
b = coordmap[i].range;
if (x <= b.right + expand && x >= b.left - expand && y <= b.bottom + expand && y >= b.top - expand) {
matches.push(coordmap[i]);
// Find distance from edges for x and y
var xdist = 0;
var ydist = 0;
if (x > b.right && x <= b.right + expand) {
xdist = x - b.right;
} else if (x < b.left && x >= b.left - expand) {
xdist = x - b.left;
}
if (y > b.bottom && y <= b.bottom + expand) {
ydist = y - b.bottom;
} else if (y < b.top && y >= b.top - expand) {
ydist = y - b.top;
}
// Cartesian distance
dists.push(Math.sqrt(Math.pow(xdist, 2) + Math.pow(ydist, 2)));
}
}
if (matches.length) {
// Find shortest distance
var min_dist = Math.min.apply(null, dists);
for (i = 0; i < matches.length; i++) {
if (dists[i] === min_dist) {
return matches[i];
}
}
}
return null;
};
// Is an offset in a panel? If supplied, `expand` tells us to expand the
// panels by that many pixels in all directions.
coordmap.isInPanel = function (offset, expand) {
expand = expand || 0;
if (coordmap.getPanel(offset, expand)) return true;
return false;
};
// Returns a function that sends mouse coordinates, scaled to data space.
// If that function is passed a null event, it will send null.
coordmap.mouseCoordinateSender = function (inputId, clip, nullOutside) {
if (clip === undefined) clip = true;
if (nullOutside === undefined) nullOutside = false;
return function (e) {
if (e === null) {
exports.onInputChange(inputId, null);
return;
}
var offset = coordmap.mouseOffset(e);
// If outside of plotting region
if (!coordmap.isInPanel(offset)) {
if (nullOutside) {
exports.onInputChange(inputId, null);
return;
}
if (clip) return;
}
if (clip && !coordmap.isInPanel(offset)) return;
var panel = coordmap.getPanel(offset);
var coords = panel.scaleInv(offset);
// Add the panel (facet) variables, if present
$.extend(coords, panel.panel_vars);
// Add variable name mappings
coords.mapping = panel.mapping;
// Add scaling information
coords.domain = panel.domain;
coords.range = panel.range;
coords.log = panel.log;
coords[".nonce"] = Math.random();
exports.onInputChange(inputId, coords);
};
};
};
// This object provides two public event listeners: mousedown, and
// dblclickIE8.
// We need to make sure that, when the image is listening for double-
// clicks, that a double-click doesn't trigger two click events. We'll
// trigger custom mousedown2 and dblclick2 events with this mousedown
// listener.
imageutils.createClickInfo = function ($el, dblclickId, dblclickDelay) {
var clickTimer = null;
var pending_e = null; // A pending mousedown2 event
// Create a new event of type eventType (like 'mousedown2'), and trigger
// it with the information stored in this.e.
function triggerEvent(newEventType, e) {
// Extract important info from e and construct a new event with type
// eventType.
var e2 = $.Event(newEventType, {
which: e.which,
pageX: e.pageX,
pageY: e.pageY,
offsetX: e.offsetX,
offsetY: e.offsetY
});
$el.trigger(e2);
}
function triggerPendingMousedown2() {
// It's possible that between the scheduling of a mousedown2 and the
// time this callback is executed, someone else triggers a
// mousedown2, so check for that.
if (pending_e) {
triggerEvent('mousedown2', pending_e);
pending_e = null;
}
}
// Set a timer to trigger a mousedown2 event, using information from the
// last recorded mousdown event.
function scheduleMousedown2(e) {
pending_e = e;
clickTimer = setTimeout(function () {
triggerPendingMousedown2();
}, dblclickDelay);
}
function mousedown(e) {
// Listen for left mouse button only
if (e.which !== 1) return;
// If no dblclick listener, immediately trigger a mousedown2 event.
if (!dblclickId) {
triggerEvent('mousedown2', e);
return;
}
// If there's a dblclick listener, make sure not to count this as a
// click on the first mousedown; we need to wait for the dblclick
// delay before we can be sure this click was a single-click.
if (pending_e === null) {
scheduleMousedown2(e);
} else {
clearTimeout(clickTimer);
// If second click is too far away, it doesn't count as a double
// click. Instead, immediately trigger a mousedown2 for the previous
// click, and set this click as a new first click.
if (pending_e && Math.abs(pending_e.offsetX - e.offsetX) > 2 || Math.abs(pending_e.offsetY - e.offsetY) > 2) {
triggerPendingMousedown2();
scheduleMousedown2(e);
} else {
// The second click was close to the first one. If it happened
// within specified delay, trigger our custom 'dblclick2' event.
pending_e = null;
triggerEvent('dblclick2', e);
}
}
}
// IE8 needs a special hack because when you do a double-click it doesn't
// trigger the click event twice - it directly triggers dblclick.
function dblclickIE8(e) {
e.which = 1; // In IE8, e.which is 0 instead of 1. ???
triggerEvent('dblclick2', e);
}
return {
mousedown: mousedown,
dblclickIE8: dblclickIE8
};
};
// ----------------------------------------------------------
// Handler creators for click, hover, brush.
// Each of these returns an object with a few public members. These public
// members are callbacks that are meant to be bound to events on $el with
// the same name (like 'mousedown').
// ----------------------------------------------------------
imageutils.createClickHandler = function (inputId, clip, coordmap) {
var clickInfoSender = coordmap.mouseCoordinateSender(inputId, clip);
return {
mousedown: function mousedown(e) {
// Listen for left mouse button only
if (e.which !== 1) return;
clickInfoSender(e);
},
onResetImg: function onResetImg() {
clickInfoSender(null);
}
};
};
imageutils.createHoverHandler = function (inputId, delay, delayType, clip, nullOutside, coordmap) {
var sendHoverInfo = coordmap.mouseCoordinateSender(inputId, clip, nullOutside);
var hoverInfoSender;
if (delayType === 'throttle') hoverInfoSender = new Throttler(null, sendHoverInfo, delay);else hoverInfoSender = new Debouncer(null, sendHoverInfo, delay);
// What to do when mouse exits the image
var mouseout;
if (nullOutside) mouseout = function mouseout() {
hoverInfoSender.normalCall(null);
};else mouseout = function mouseout() {};
return {
mousemove: function mousemove(e) {
hoverInfoSender.normalCall(e);
},
mouseout: mouseout,
onResetImg: function onResetImg() {
hoverInfoSender.immediateCall(null);
}
};
};
// Returns a brush handler object. This has three public functions:
// mousedown, mousemove, and onResetImg.
imageutils.createBrushHandler = function (inputId, $el, opts, coordmap, outputId) {
// Parameter: expand the area in which a brush can be started, by this
// many pixels in all directions. (This should probably be a brush option)
var expandPixels = 20;
// Represents the state of the brush
var brush = imageutils.createBrush($el, opts, coordmap, expandPixels);
// Brush IDs can span multiple image/plot outputs. When an output is brushed,
// if a brush with the same ID is active on a different image/plot, it must
// be dismissed (but without sending any data to the server). We implement
// this by sending the shiny-internal:brushed event to all plots, and letting
// each plot decide for itself what to do.
//
// The decision to have the event sent to each plot (as opposed to a single
// event triggered on, say, the document) was made to make cleanup easier;
// listening on an event on the document would prevent garbage collection
// of plot outputs that are removed from the document.
$el.on("shiny-internal:brushed.image_output", function (e, coords) {
// If the new brush shares our ID but not our output element ID, we
// need to clear our brush (if any).
if (coords.brushId === inputId && coords.outputId !== outputId) {
$el.data("mostRecentBrush", false);
brush.reset();
}
});
// Set cursor to one of 7 styles. We need to set the cursor on the whole
// el instead of the brush div, because the brush div has
// 'pointer-events:none' so that it won't intercept pointer events.
// If `style` is null, don't add a cursor style.
function setCursorStyle(style) {
$el.removeClass('crosshair grabbable grabbing ns-resize ew-resize nesw-resize nwse-resize');
if (style) $el.addClass(style);
}
function sendBrushInfo() {
var coords = brush.boundsData();
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.onInputChange(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
});
return;
}
var panel = brush.getPanel();
// Add the panel (facet) variables, if present
$.extend(coords, panel.panel_vars);
// Add variable name mappings
coords.mapping = panel.mapping;
// Add scaling information
coords.domain = panel.domain;
coords.range = panel.range;
coords.log = panel.log;
coords.direction = opts.brushDirection;
coords.brushId = inputId;
coords.outputId = outputId;
// Send data to server
exports.onInputChange(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
}
var brushInfoSender;
if (opts.brushDelayType === 'throttle') {
brushInfoSender = new Throttler(null, sendBrushInfo, opts.brushDelay);
} else {
brushInfoSender = new Debouncer(null, sendBrushInfo, opts.brushDelay);
}
function mousedown(e) {
// This can happen when mousedown inside the graphic, then mouseup
// outside, then mousedown inside. Just ignore the second
// mousedown.
if (brush.isBrushing() || brush.isDragging() || brush.isResizing()) return;
// Listen for left mouse button only
if (e.which !== 1) return;
var offset = coordmap.mouseOffset(e);
// Ignore mousedown events outside of plotting region, expanded by
// a number of pixels specified in expandPixels.
if (opts.brushClip && !coordmap.isInPanel(offset, expandPixels)) return;
brush.up({ x: NaN, y: NaN });
brush.down(offset);
if (brush.isInResizeArea(offset)) {
brush.startResizing(offset);
// Attach the move and up handlers to the window so that they respond
// even when the mouse is moved outside of the image.
$(document).on('mousemove.image_brush', mousemoveResizing).on('mouseup.image_brush', mouseupResizing);
} else if (brush.isInsideBrush(offset)) {
brush.startDragging(offset);
setCursorStyle('grabbing');
// Attach the move and up handlers to the window so that they respond
// even when the mouse is moved outside of the image.
$(document).on('mousemove.image_brush', mousemoveDragging).on('mouseup.image_brush', mouseupDragging);
} else {
var panel = coordmap.getPanel(offset, expandPixels);
brush.startBrushing(panel.clip(offset));
// Attach the move and up handlers to the window so that they respond
// even when the mouse is moved outside of the image.
$(document).on('mousemove.image_brush', mousemoveBrushing).on('mouseup.image_brush', mouseupBrushing);
}
}
// This sets the cursor style when it's in the el
function mousemove(e) {
var offset = coordmap.mouseOffset(e);
if (!(brush.isBrushing() || brush.isDragging() || brush.isResizing())) {
// Set the cursor depending on where it is
if (brush.isInResizeArea(offset)) {
var r = brush.whichResizeSides(offset);
if (r.left && r.top || r.right && r.bottom) {
setCursorStyle('nwse-resize');
} else if (r.left && r.bottom || r.right && r.top) {
setCursorStyle('nesw-resize');
} else if (r.left || r.right) {
setCursorStyle('ew-resize');
} else if (r.top || r.bottom) {
setCursorStyle('ns-resize');
}
} else if (brush.isInsideBrush(offset)) {
setCursorStyle('grabbable');
} else if (coordmap.isInPanel(offset, expandPixels)) {
setCursorStyle('crosshair');
} else {
setCursorStyle(null);
}
}
}
// mousemove handlers while brushing or dragging
function mousemoveBrushing(e) {
brush.brushTo(coordmap.mouseOffset(e));
brushInfoSender.normalCall();
}
function mousemoveDragging(e) {
brush.dragTo(coordmap.mouseOffset(e));
brushInfoSender.normalCall();
}
function mousemoveResizing(e) {
brush.resizeTo(coordmap.mouseOffset(e));
brushInfoSender.normalCall();
}
// mouseup handlers while brushing or dragging
function mouseupBrushing(e) {
// Listen for left mouse button only
if (e.which !== 1) return;
$(document).off('mousemove.image_brush').off('mouseup.image_brush');
brush.up(coordmap.mouseOffset(e));
brush.stopBrushing();
setCursorStyle('crosshair');
// If the brush didn't go anywhere, hide the brush, clear value,
// and return.
if (brush.down().x === brush.up().x && brush.down().y === brush.up().y) {
brush.reset();
brushInfoSender.immediateCall();
return;
}
// Send info immediately on mouseup, but only if needed. If we don't
// do the pending check, we might send the same data twice (with
// with difference nonce).
if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
}
function mouseupDragging(e) {
// Listen for left mouse button only
if (e.which !== 1) return;
$(document).off('mousemove.image_brush').off('mouseup.image_brush');
brush.up(coordmap.mouseOffset(e));
brush.stopDragging();
setCursorStyle('grabbable');
if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
}
function mouseupResizing(e) {
// Listen for left mouse button only
if (e.which !== 1) return;
$(document).off('mousemove.image_brush').off('mouseup.image_brush');
brush.up(coordmap.mouseOffset(e));
brush.stopResizing();
if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
}
// Brush maintenance: When an image is re-rendered, the brush must either
// be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The
// "mostRecentBrush" bit is to ensure that when multiple outputs share the
// same brush ID, inactive brushes don't send null values up to the server.
// This should be called when the img (not the el) is reset
function onResetImg() {
if (opts.brushResetOnNew) {
if ($el.data("mostRecentBrush")) {
brush.reset();
brushInfoSender.immediateCall();
}
}
}
if (!opts.brushResetOnNew) {
if ($el.data("mostRecentBrush")) {
brush.importOldBrush();
brushInfoSender.immediateCall();
}
}
return {
mousedown: mousedown,
mousemove: mousemove,
onResetImg: onResetImg
};
};
// Returns an object that represents the state of the brush. This gets wrapped
// in a brushHandler, which provides various event listeners.
imageutils.createBrush = function ($el, opts, coordmap, expandPixels) {
// Number of pixels outside of brush to allow start resizing
var resizeExpand = 10;
var el = $el[0];
var $div = null; // The div representing the brush
var state = {};
reset();
function reset() {
// Current brushing/dragging/resizing state
state.brushing = false;
state.dragging = false;
state.resizing = false;
// Offset of last mouse down and up events
state.down = { x: NaN, y: NaN };
state.up = { x: NaN, y: NaN };
// Which side(s) we're currently resizing
state.resizeSides = {
left: false,
right: false,
top: false,
bottom: false
};
// Bounding rectangle of the brush, in pixel and data dimensions. We need to
// record data dimensions along with pixel dimensions so that when a new
// plot is sent, we can re-draw the brush div with the appropriate coords.
state.boundsPx = {
xmin: NaN,
xmax: NaN,
ymin: NaN,
ymax: NaN
};
state.boundsData = {
xmin: NaN,
xmax: NaN,
ymin: NaN,
ymax: NaN
};
// Panel object that the brush is in
state.panel = null;
// The bounds at the start of a drag/resize
state.changeStartBounds = {
xmin: NaN,
xmax: NaN,
ymin: NaN,
ymax: NaN
};
if ($div) $div.remove();
}
// If there's an existing brush div, use that div to set the new brush's
// settings, provided that the x, y, and panel variables have the same names,
// and there's a panel with matching panel variable values.
function importOldBrush() {
var oldDiv = $el.find('#' + el.id + '_brush');
if (oldDiv.length === 0) return;
var oldBoundsData = oldDiv.data('bounds-data');
var oldPanel = oldDiv.data('panel');
if (!oldBoundsData || !oldPanel) return;
// Compare two objects. This checks that objects a and b have the same est
// of keys, and that each key has the same value. This function isn't
// perfect, but it's good enough for comparing variable mappings, below.
function isEquivalent(a, b) {
if (a === undefined) {
if (b === undefined) return true;else return false;
}
if (a === null) {
if (b === null) return true;else return false;
}
var aProps = Object.getOwnPropertyNames(a);
var bProps = Object.getOwnPropertyNames(b);
if (aProps.length != bProps.length) return false;
for (var i = 0; i < aProps.length; i++) {
var propName = aProps[i];
if (a[propName] !== b[propName]) {
return false;
}
}
return true;
}
// Find a panel that has matching vars; if none found, we can't restore.
// The oldPanel and new panel must match on their mapping vars, and the
// values.
for (var i = 0; i < coordmap.length; i++) {
var curPanel = coordmap[i];
if (isEquivalent(oldPanel.mapping, curPanel.mapping) && isEquivalent(oldPanel.panel_vars, curPanel.panel_vars)) {
// We've found a matching panel
state.panel = coordmap[i];
break;
}
}
// If we didn't find a matching panel, remove the old div and return
if (state.panel === null) {
oldDiv.remove();
return;
}
$div = oldDiv;
boundsData(oldBoundsData);
updateDiv();
}
// Return true if the offset is inside min/max coords
function isInsideBrush(offset) {
var bounds = state.boundsPx;
return offset.x <= bounds.xmax && offset.x >= bounds.xmin && offset.y <= bounds.ymax && offset.y >= bounds.ymin;
}
// Return true if offset is inside a region to start a resize
function isInResizeArea(offset) {
var sides = whichResizeSides(offset);
return sides.left || sides.right || sides.top || sides.bottom;
}
// Return an object representing which resize region(s) the cursor is in.
function whichResizeSides(offset) {
var b = state.boundsPx;
// Bounds with expansion
var e = {
xmin: b.xmin - resizeExpand,
xmax: b.xmax + resizeExpand,
ymin: b.ymin - resizeExpand,
ymax: b.ymax + resizeExpand
};
var res = {
left: false,
right: false,
top: false,
bottom: false
};
if ((opts.brushDirection === 'xy' || opts.brushDirection === 'x') && offset.y <= e.ymax && offset.y >= e.ymin) {
if (offset.x < b.xmin && offset.x >= e.xmin) res.left = true;else if (offset.x > b.xmax && offset.x <= e.xmax) res.right = true;
}
if ((opts.brushDirection === 'xy' || opts.brushDirection === 'y') && offset.x <= e.xmax && offset.x >= e.xmin) {
if (offset.y < b.ymin && offset.y >= e.ymin) res.top = true;else if (offset.y > b.ymax && offset.y <= e.ymax) res.bottom = true;
}
return res;
}
// Sets the bounds of the brush, given a box and optional panel. This
// will fit the box bounds into the panel, so we don't brush outside of it.
// This knows whether we're brushing in the x, y, or xy directions, and sets
// bounds accordingly.
// If no box is passed in, just return current bounds.
function boundsPx(box) {
if (box === undefined) return state.boundsPx;
var min = { x: box.xmin, y: box.ymin };
var max = { x: box.xmax, y: box.ymax };
var panel = state.panel;
var panelBounds = panel.range;
if (opts.brushClip) {
min = panel.clip(min);
max = panel.clip(max);
}
if (opts.brushDirection === 'xy') {
// No change
} else if (opts.brushDirection === 'x') {
// Extend top and bottom of plotting area
min.y = panelBounds.top;
max.y = panelBounds.bottom;
} else if (opts.brushDirection === 'y') {
min.x = panelBounds.left;
max.x = panelBounds.right;
}
state.boundsPx = {
xmin: min.x,
xmax: max.x,
ymin: min.y,
ymax: max.y
};
// Positions in data space
var minData = state.panel.scaleInv(min);
var maxData = state.panel.scaleInv(max);
// For reversed scales, the min and max can be reversed, so use findBox
// to ensure correct order.
state.boundsData = coordmap.findBox(minData, maxData);
// We also need to attach the data bounds and panel as data attributes, so
// that if the image is re-sent, we can grab the data bounds to create a new
// brush. This should be fast because it doesn't actually modify the DOM.
$div.data('bounds-data', state.boundsData);
$div.data('panel', state.panel);
return undefined;
}
// Get or set the bounds of the brush using coordinates in the data space.
function boundsData(box) {
if (box === undefined) {
return state.boundsData;
}
var min = { x: box.xmin, y: box.ymin };
var max = { x: box.xmax, y: box.ymax };
var minPx = state.panel.scale(min);
var maxPx = state.panel.scale(max);
// The scaling function can reverse the direction of the axes, so we need to
// find the min and max again.
boundsPx({
xmin: Math.min(minPx.x, maxPx.x),
xmax: Math.max(minPx.x, maxPx.x),
ymin: Math.min(minPx.y, maxPx.y),
ymax: Math.max(minPx.y, maxPx.y)
});
return undefined;
}
function getPanel() {
return state.panel;
}
// Add a new div representing the brush.
function addDiv() {
if ($div) $div.remove();
// Start hidden; we'll show it when movement occurs
$div = $(document.createElement('div')).attr('id', el.id + '_brush').css({
'background-color': opts.brushFill,
'opacity': opts.brushOpacity,
'pointer-events': 'none',
'position': 'absolute'
}).hide();
var borderStyle = '1px solid ' + opts.brushStroke;
if (opts.brushDirection === 'xy') {
$div.css({
'border': borderStyle
});
} else if (opts.brushDirection === 'x') {
$div.css({
'border-left': borderStyle,
'border-right': borderStyle
});
} else if (opts.brushDirection === 'y') {
$div.css({
'border-top': borderStyle,
'border-bottom': borderStyle
});
}
$el.append($div);
$div.offset({ x: 0, y: 0 }).width(0).outerHeight(0);
}
// Update the brush div to reflect the current brush bounds.
function updateDiv() {
// Need parent offset relative to page to calculate mouse offset
// relative to page.
var imgOffset = $el.offset();
var b = state.boundsPx;
$div.offset({
top: imgOffset.top + b.ymin,
left: imgOffset.left + b.xmin
}).outerWidth(b.xmax - b.xmin + 1).outerHeight(b.ymax - b.ymin + 1);
}
function down(offset) {
if (offset === undefined) return state.down;
state.down = offset;
return undefined;
}
function up(offset) {
if (offset === undefined) return state.up;
state.up = offset;
return undefined;
}
function isBrushing() {
return state.brushing;
}
function startBrushing() {
state.brushing = true;
addDiv();
state.panel = coordmap.getPanel(state.down, expandPixels);
boundsPx(coordmap.findBox(state.down, state.down));
updateDiv();
}
function brushTo(offset) {
boundsPx(coordmap.findBox(state.down, offset));
$div.show();
updateDiv();
}
function stopBrushing() {
state.brushing = false;
// Save the final bounding box of the brush
boundsPx(coordmap.findBox(state.down, state.up));
}
function isDragging() {
return state.dragging;
}
function startDragging() {
state.dragging = true;
state.changeStartBounds = $.extend({}, state.boundsPx);
}
function dragTo(offset) {
// How far the brush was dragged
var dx = offset.x - state.down.x;
var dy = offset.y - state.down.y;
// Calculate what new positions would be, before clipping.
var start = state.changeStartBounds;
var newBounds = {
xmin: start.xmin + dx,
xmax: start.xmax + dx,
ymin: start.ymin + dy,
ymax: start.ymax + dy
};
// Clip to the plotting area
if (opts.brushClip) {
var panelBounds = state.panel.range;
// Convert to format for shiftToRange
var xvals = [newBounds.xmin, newBounds.xmax];
var yvals = [newBounds.ymin, newBounds.ymax];
xvals = coordmap.shiftToRange(xvals, panelBounds.left, panelBounds.right);
yvals = coordmap.shiftToRange(yvals, panelBounds.top, panelBounds.bottom);
// Convert back to bounds format
newBounds = {
xmin: xvals[0],
xmax: xvals[1],
ymin: yvals[0],
ymax: yvals[1]
};
}
boundsPx(newBounds);
updateDiv();
}
function stopDragging() {
state.dragging = false;
}
function isResizing() {
return state.resizing;
}
function startResizing() {
state.resizing = true;
state.changeStartBounds = $.extend({}, state.boundsPx);
state.resizeSides = whichResizeSides(state.down);
}
function resizeTo(offset) {
// How far the brush was dragged
var dx = offset.x - state.down.x;
var dy = offset.y - state.down.y;
// Calculate what new positions would be, before clipping.
var b = $.extend({}, state.changeStartBounds);
var panelBounds = state.panel.range;
if (state.resizeSides.left) {
b.xmin = coordmap.shiftToRange([b.xmin + dx], panelBounds.left, b.xmax)[0];
} else if (state.resizeSides.right) {
b.xmax = coordmap.shiftToRange([b.xmax + dx], b.xmin, panelBounds.right)[0];
}
if (state.resizeSides.top) {
b.ymin = coordmap.shiftToRange([b.ymin + dy], panelBounds.top, b.ymax)[0];
} else if (state.resizeSides.bottom) {
b.ymax = coordmap.shiftToRange([b.ymax + dy], b.ymin, panelBounds.bottom)[0];
}
boundsPx(b);
updateDiv();
}
function stopResizing() {
state.resizing = false;
}
return {
reset: reset,
importOldBrush: importOldBrush,
isInsideBrush: isInsideBrush,
isInResizeArea: isInResizeArea,
whichResizeSides: whichResizeSides,
boundsPx: boundsPx,
boundsData: boundsData,
getPanel: getPanel,
down: down,
up: up,
isBrushing: isBrushing,
startBrushing: startBrushing,
brushTo: brushTo,
stopBrushing: stopBrushing,
isDragging: isDragging,
startDragging: startDragging,
dragTo: dragTo,
stopDragging: stopDragging,
isResizing: isResizing,
startResizing: startResizing,
resizeTo: resizeTo,
stopResizing: stopResizing
};
};
exports.resetBrush = function (brushId) {
exports.onInputChange(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});
};
//---------------------------------------------------------------------
// Source file: ../srcjs/output_binding_html.js
var htmlOutputBinding = new OutputBinding();
$.extend(htmlOutputBinding, {
find: function find(scope) {
return $(scope).find('.shiny-html-output');
},
onValueError: function onValueError(el, err) {
exports.unbindAll(el);
this.renderError(el, err);
},
renderValue: function renderValue(el, data) {
exports.renderContent(el, data);
}
});
outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput');
var renderDependencies = exports.renderDependencies = function (dependencies) {
if (dependencies) {
$.each(dependencies, function (i, dep) {
renderDependency(dep);
});
}
};
// Render HTML in a DOM element, add dependencies, and bind Shiny
// inputs/outputs. `content` can be null, a string, or an object with
// properties 'html' and 'deps'.
exports.renderContent = function (el, content) {
var where = arguments.length <= 2 || arguments[2] === undefined ? "replace" : arguments[2];
exports.unbindAll(el);
var html;
var dependencies = [];
if (content === null) {
html = '';
} else if (typeof content === 'string') {
html = content;
} else if ((typeof content === 'undefined' ? 'undefined' : _typeof(content)) === 'object') {
html = content.html;
dependencies = content.deps || [];
}
exports.renderHtml(html, el, dependencies, where);
var scope = el;
if (where === "replace") {
exports.initializeInputs(el);
exports.bindAll(el);
} else {
var $parent = $(el).parent();
if ($parent.length > 0) {
scope = $parent;
if (where === "beforeBegin" || where === "afterEnd") {
var $grandparent = $parent.parent();
if ($grandparent.length > 0) scope = $grandparent;
}
}
exports.initializeInputs(scope);
exports.bindAll(scope);
}
};
// Render HTML in a DOM element, inserting singletons into head as needed
exports.renderHtml = function (html, el, dependencies) {
var where = arguments.length <= 3 || arguments[3] === undefined ? 'replace' : arguments[3];
renderDependencies(dependencies);
return singletons.renderHtml(html, el, where);
};
var htmlDependencies = {};
function registerDependency(name, version) {
htmlDependencies[name] = version;
}
// Client-side dependency resolution and rendering
function renderDependency(dep) {
if (htmlDependencies.hasOwnProperty(dep.name)) return false;
registerDependency(dep.name, dep.version);
var href = dep.src.href;
var $head = $("head").first();
if (dep.meta) {
var metas = $.map(asArray(dep.meta), function (content, name) {
return $("").attr("name", name).attr("content", content);
});
$head.append(metas);
}
if (dep.stylesheet) {
var stylesheets = $.map(asArray(dep.stylesheet), function (stylesheet) {
return $("").attr("href", href + "/" + encodeURI(stylesheet));
});
$head.append(stylesheets);
}
if (dep.script) {
var scripts = $.map(asArray(dep.script), function (scriptName) {
return $("