'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 && obj !== Symbol.prototype ? "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) { // Remove any previously defined handlers so that only the most recent one // will be called if (customMessageHandlers[type]) { var typeIdx = customMessageHandlerOrder.indexOf(type); if (typeIdx !== -1) { customMessageHandlerOrder.splice(typeIdx, 1); delete customMessageHandlers[type]; } } 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 (data) { var msgObj = {}; if (typeof data === "string") { msgObj = JSON.parse(data); } else { // data is arraybuffer var len = new DataView(data, 0, 1).getUint8(0); var typedv = new DataView(data, 1, len); var typebuf = []; for (var i = 0; i < len; i++) { typebuf.push(String.fromCharCode(typedv.getUint8(i))); } var type = typebuf.join(""); data = data.slice(len + 1); msgObj.custom = {}; msgObj.custom[type] = data; } 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 console.warn('The selector you chose ("' + message.selector + '") could not be found in the DOM.'); 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) { if (message.style === "notification") { // For new-style (starting in Shiny 0.14) progress indicators that use // the notification API. // 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 }); } else if (message.style === "old") { // For old-style (Shiny <=0.13.2) progress indicators. // Add progress container (for all progress items) if not already present var $container = $('.shiny-progress-container'); if ($container.length === 0) { $container = $('
'); $('body').append($container); } // Add div for just this progress ID var depth = $('.shiny-progress.open').length; // The 'bar' class is needed for backward compatibility with Bootstrap 2. var $progress = $('
' + '
' + '
' + 'message' + '' + '
' + '
'); $progress.attr('id', message.id); $container.append($progress); // Stack bars var $progressBar = $progress.find('.progress'); $progressBar.css('top', depth * $progressBar.height() + 'px'); // Stack text objects var $progressText = $progress.find('.progress-text'); $progressText.css('top', 3 * $progressBar.height() + depth * $progressText.outerHeight() + 'px'); $progress.hide(); } }, // Update page-level progress bar update: function update(message) { if (message.style === "notification") { // For new-style (starting in Shiny 0.14) progress indicators that use // the notification API. 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(); } } } else if (message.style === "old") { // For old-style (Shiny <=0.13.2) progress indicators. var $progress = $('#' + message.id + '.shiny-progress'); 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('.bar').width(message.value * 100 + '%'); } else { $progress.find('.progress').hide(); } } $progress.fadeIn(); } }, // Close page-level progress bar close: function close(message) { if (message.style === "notification") { exports.notifications.remove(message.id); } else if (message.style === "old") { var $progress = $('#' + message.id + '.shiny-progress'); $progress.removeClass('open'); $progress.fadeOut({ complete: function complete() { $progress.remove(); // If this was the last shiny-progress, remove container if ($('.shiny-progress').length === 0) $('.shiny-progress-container').remove(); } }); } } }; exports.progressHandlers = progressHandlers; // Returns a URL which can be queried to get values from inside the server // function. This is enabled with `options(shiny.testmode=TRUE)`. this.getTestSnapshotBaseUrl = function () { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var _ref$fullUrl = _ref.fullUrl; var fullUrl = _ref$fullUrl === undefined ? true : _ref$fullUrl; var loc = window.location; var url = ""; if (fullUrl) { // Strip off everything after last slash in path, like dirname() in R url = loc.origin + loc.pathname.replace(/\/[^/]*$/, ""); } url += "/session/" + encodeURIComponent(this.config.sessionId) + "/dataobj/shinytest?w=" + encodeURIComponent(this.config.workerId) + "&nonce=" + randomId(); return url; }; }).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 _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var _ref2$html = _ref2.html; var html = _ref2$html === undefined ? '' : _ref2$html; var _ref2$action = _ref2.action; var action = _ref2$action === undefined ? '' : _ref2$action; var _ref2$deps = _ref2.deps; var deps = _ref2$deps === undefined ? [] : _ref2$deps; var _ref2$duration = _ref2.duration; var duration = _ref2$duration === undefined ? 5000 : _ref2$duration; var _ref2$id = _ref2.id; var id = _ref2$id === undefined ? null : _ref2$id; var _ref2$closeButton = _ref2.closeButton; var closeButton = _ref2$closeButton === undefined ? true : _ref2$closeButton; var _ref2$type = _ref2.type; var type = _ref2$type === undefined ? null : _ref2$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 _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var _ref3$html = _ref3.html; var html = _ref3$html === undefined ? '' : _ref3$html; var _ref3$deps = _ref3.deps; var deps = _ref3$deps === undefined ? [] : _ref3$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 (e) { if (e.target === $("#shiny-modal")[0]) { exports.unbindAll($modal); $modal.remove(); } }); } $modal.on('keydown.shinymodal', function (e) { // If we're listening for Esc, don't let the event propagate. See // https://github.com/rstudio/shiny/issues/1453. The value of // data("keyboard") needs to be checked inside the handler, because at // the time that $modal.on() is called, the $("#shiny-modal") div doesn't // yet exist. if ($("#shiny-modal").data("keyboard") === false) return; if (e.keyCode === 27) { e.stopPropagation(); e.preventDefault(); } }); // Set/replace contents of wrapper with html. exports.renderContent($modal, { html: html, deps: deps }); }, remove: function remove() { var $modal = $('#shiny-modal-wrapper'); $modal.off('keydown.shinymodal'); // 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 ? arguments[2] : "replace"; if (where === "replace") { exports.unbindAll(el); } 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 ? arguments[3] : 'replace'; 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 (obj, idx) { // only one named pair is expected in obj as it's already been decomposed var name = Object.keys(obj)[0]; return $("").attr("name", name).attr("content", obj[name]); }); $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 $("