"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; }; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } //--------------------------------------------------------------------- // Source file: ../srcjs/_start.js (function () { var $ = jQuery; var exports = window.Shiny = window.Shiny || {}; exports.version = "1.2.0"; // Version number inserted by Grunt var origPushState = window.history.pushState; window.history.pushState = function () { var result = origPushState.apply(this, arguments); $(document).trigger("pushstate"); return result; }; $(document).on('submit', 'form:not([action])', function (e) { e.preventDefault(); }); //--------------------------------------------------------------------- // Source file: ../srcjs/utils.js function escapeHTML(str) { var escaped = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" }; return str.replace(/[&<>'"\/]/g, function (m) { return escaped[m]; }); } 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; } // Round to a specified number of significant digits. function roundSignif(x) { var digits = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; if (digits < 1) throw "Significant digits must be at least 1."; // This converts to a string and back to a number, which is inelegant, but // is less prone to FP rounding error than an alternate method which used // Math.round(). return parseFloat(x.toPrecision(digits)); } // 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 expr_escaped = expr.replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0').replace(/\n/g, '\\n').replace(/\r/g, '\\r') // \b has a special meaning; need [\b] to match backspace char. .replace(/[\b]/g, '\\b'); try { var func = new Function("with (this) {\n try {\n return (" + expr + ");\n } catch (e) {\n console.error('Error evaluating expression: " + expr_escaped + "');\n throw e;\n }\n }"); } catch (e) { console.error("Error parsing expression: " + expr); throw e; } 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'); }; // Maps a function over an object, preserving keys. Like the mapValues // function from lodash. function mapValues(obj, f) { var newObj = {}; for (var key in obj) { if (obj.hasOwnProperty(key)) newObj[key] = f(obj[key], key, obj); } return newObj; } // This is does the same as Number.isNaN, but that function unfortunately does // not exist in any version of IE. function isnan(x) { return typeof x === 'number' && isNaN(x); } // Binary equality function used by the equal function. function _equal(x, y) { if ($.type(x) === "object" && $.type(y) === "object") { if (Object.keys(x).length !== Object.keys(y).length) return false; for (var prop in x) { if (!y.hasOwnProperty(prop) || !_equal(x[prop], y[prop])) return false; }return true; } else if ($.type(x) === "array" && $.type(y) === "array") { if (x.length !== y.length) return false; for (var i = 0; i < x.length; i++) { if (!_equal(x[i], y[i])) return false; }return true; } else { return x === y; } } // Structural or "deep" equality predicate. Tests two or more arguments for // equality, traversing arrays and objects (as determined by $.type) as // necessary. // // Objects other than objects and arrays are tested for equality using ===. function equal() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } if (args.length < 2) throw new Error("equal requires at least two arguments."); for (var i = 0; i < args.length - 1; i++) { if (!_equal(args[i], args[i + 1])) return false; } return true; }; // Compare version strings like "1.0.1", "1.4-2". `op` must be a string like // "==" or "<". exports.compareVersion = function (a, op, b) { function versionParts(ver) { return (ver + "").replace(/-/, ".").replace(/(\.0)+[^\.]*$/, "").split("."); } function cmpVersion(a, b) { a = versionParts(a); b = versionParts(b); var len = Math.min(a.length, b.length); var cmp; for (var i = 0; i < len; i++) { cmp = parseInt(a[i], 10) - parseInt(b[i], 10); if (cmp !== 0) { return cmp; } } return a.length - b.length; } var diff = cmpVersion(a, b); if (op === "==") return diff === 0;else if (op === ">=") return diff >= 0;else if (op === ">") return diff > 0;else if (op === "<=") return diff <= 0;else if (op === "<") return diff < 0;else throw "Unknown operator: " + op; }; //--------------------------------------------------------------------- // 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, opts) { this.pendingData[name] = value; if (!this.reentrant) { if (opts.priority === "event") { this.$sendNow(); } else if (!this.timerId) { this.timerId = setTimeout(this.$sendNow.bind(this), 0); } } }; this.$sendNow = function () { if (this.reentrant) { console.trace("Unexpected reentrancy in InputBatchSender!"); } this.reentrant = true; try { this.timerId = null; $.each(this.lastChanceCallback, function (i, callback) { callback(); }); var currentData = this.pendingData; this.pendingData = {}; this.shinyapp.sendInput(currentData); } finally { this.reentrant = false; } }; }).call(InputBatchSender.prototype); var InputNoResendDecorator = function InputNoResendDecorator(target, initialValues) { this.target = target; this.lastSentValues = this.reset(initialValues); }; (function () { this.setInput = function (name, value, opts) { var _splitInputNameType = splitInputNameType(name); var inputName = _splitInputNameType.name; var inputType = _splitInputNameType.inputType; var jsonValue = JSON.stringify(value); if (opts.priority !== "event" && this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) { return; } this.lastSentValues[inputName] = { jsonValue: jsonValue, inputType: inputType }; this.target.setInput(name, value, opts); }; this.reset = function () { var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Given an object with flat name-value format: // { x: "abc", "y.shiny.number": 123 } // Create an object in cache format and save it: // { x: { jsonValue: '"abc"', inputType: "" }, // y: { jsonValue: "123", inputType: "shiny.number" } } var cacheValues = {}; for (var inputName in values) { if (values.hasOwnProperty(inputName)) { var _splitInputNameType2 = splitInputNameType(inputName); var name = _splitInputNameType2.name; var inputType = _splitInputNameType2.inputType; cacheValues[name] = { jsonValue: JSON.stringify(values[inputName]), inputType: inputType }; } } this.lastSentValues = cacheValues; }; }).call(InputNoResendDecorator.prototype); var InputEventDecorator = function InputEventDecorator(target) { this.target = target; }; (function () { this.setInput = function (name, value, opts) { var evt = jQuery.Event("shiny:inputchanged"); var input = splitInputNameType(name); evt.name = input.name; evt.inputType = input.inputType; evt.value = value; evt.binding = opts.binding; evt.el = opts.el; evt.priority = opts.priority; $(document).trigger(evt); if (!evt.isDefaultPrevented()) { name = evt.name; if (evt.inputType !== '') name += ':' + evt.inputType; // Most opts aren't passed along to lower levels in the input decorator // stack. this.target.setInput(name, evt.value, { priority: opts.priority }); } }; }).call(InputEventDecorator.prototype); var InputRateDecorator = function InputRateDecorator(target) { this.target = target; this.inputRatePolicies = {}; }; (function () { this.setInput = function (name, value, opts) { this.$ensureInit(name); if (opts.priority !== "deferred") this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts); }; 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, opts) { this.target.setInput(name, value, opts); }; }).call(InputRateDecorator.prototype); var InputDeferDecorator = function InputDeferDecorator(target) { this.target = target; this.pendingInput = {}; }; (function () { this.setInput = function (name, value, opts) { if (/^\./.test(name)) this.target.setInput(name, value, opts);else this.pendingInput[name] = { value: value, opts: opts }; }; this.submit = function () { for (var name in this.pendingInput) { if (this.pendingInput.hasOwnProperty(name)) { var input = this.pendingInput[name]; this.target.setInput(name, input.value, input.opts); } } }; }).call(InputDeferDecorator.prototype); var InputValidateDecorator = function InputValidateDecorator(target) { this.target = target; }; (function () { this.setInput = function (name, value, opts) { if (!name) throw "Can't set input with empty name."; opts = addDefaultInputOpts(opts); this.target.setInput(name, value, opts); }; }).call(InputValidateDecorator.prototype); // Merge opts with defaults, and return a new object. function addDefaultInputOpts(opts) { opts = $.extend({ priority: "immediate", binding: null, el: null }, opts); if (opts && typeof opts.priority !== "undefined") { switch (opts.priority) { case "deferred": case "immediate": case "event": break; default: throw new Error("Unexpected input value mode: '" + opts.priority + "'"); } } return opts; } function splitInputNameType(name) { var name2 = name.split(':'); return { name: name2[0], inputType: name2.length > 1 ? name2[1] : '' }; } //--------------------------------------------------------------------- // Source file: ../srcjs/shinyapp.js var ShinyApp = function ShinyApp() { this.$socket = null; // Cached input values this.$inputValues = {}; // Input values at initialization (and reconnect) this.$initialInput = {}; // 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"; 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 () { if (window.parent) { window.parent.postMessage("disconnected", "*"); } }; 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) { var binding = this.$bindings[name]; var evt = jQuery.Event('shiny:value'); evt.name = name; evt.value = value; evt.binding = binding; if (this.$values[name] === value) { $(binding ? binding.el : document).trigger(evt); return undefined; } this.$values[name] = value; delete this.$errors[name]; $(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; } }; // Narrows a scopeComponent -- an input or output object -- to one constrained // by nsPrefix. Returns a new object with keys removed and renamed as // necessary. function narrowScopeComponent(scopeComponent, nsPrefix) { return Object.keys(scopeComponent).filter(function (k) { return k.indexOf(nsPrefix) === 0; }).map(function (k) { return _defineProperty({}, k.substring(nsPrefix.length), scopeComponent[k]); }).reduce(function (obj, pair) { return $.extend(obj, pair); }, {}); } // Narrows a scope -- an object with input and output "subComponents" -- to // one constrained by the nsPrefix string. // // If nsPrefix is null or empty, returns scope without modification. // // Otherwise, returns a new object with keys in subComponents removed and // renamed as necessary. function narrowScope(scope, nsPrefix) { return nsPrefix ? { input: narrowScopeComponent(scope.input, nsPrefix), output: narrowScopeComponent(scope.output, nsPrefix) } : scope; } 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 nsPrefix = el.attr('data-ns-prefix'); var nsScope = narrowScope(scope, nsPrefix); var show = condFunc(nsScope); 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 = { workerId: message.workerId, sessionId: message.sessionId }; if (message.user) exports.user = message.user; $(document).trigger('shiny:sessioninitialized'); }); 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; }); }); function getTabset(id) { var $tabset = $("#" + $escape(id)); if ($tabset.length === 0) throw "There is no tabsetPanel (or navbarPage or navlistPanel) " + "with id equal to '" + id + "'"; return $tabset; } function getTabContent($tabset) { var tabsetId = $tabset.attr("data-tabsetid"); var $tabContent = $("div.tab-content[data-tabsetid='" + $escape(tabsetId) + "']"); return $tabContent; } function getTargetTabs($tabset, $tabContent, target) { var dataValue = "[data-value='" + $escape(target) + "']"; var $aTag = $tabset.find("a" + dataValue); var $liTag = $aTag.parent(); if ($liTag.length === 0) { throw "There is no tabPanel (or navbarMenu) with value" + " (or menuName) equal to '" + target + "'"; } var $liTags = []; var $divTags = []; if ($aTag.attr("data-toggle") === "dropdown") { // dropdown var $dropdownTabset = $aTag.find("+ ul.dropdown-menu"); var dropdownId = $dropdownTabset.attr("data-tabsetid"); var $dropdownLiTags = $dropdownTabset.find("a[data-toggle='tab']").parent("li"); $dropdownLiTags.each(function (i, el) { $liTags.push($(el)); }); var selector = "div.tab-pane[id^='tab-" + $escape(dropdownId) + "']"; var $dropdownDivs = $tabContent.find(selector); $dropdownDivs.each(function (i, el) { $divTags.push($(el)); }); } else { // regular tab $divTags.push($tabContent.find("div" + dataValue)); } return { $liTag: $liTag, $liTags: $liTags, $divTags: $divTags }; } addMessageHandler("shiny-insert-tab", function (message) { var $parentTabset = getTabset(message.inputId); var $tabset = $parentTabset; var $tabContent = getTabContent($tabset); var tabsetId = $parentTabset.attr("data-tabsetid"); var $divTag = $(message.divTag.html); var $liTag = $(message.liTag.html); var $aTag = $liTag.find("> a"); // Unless the item is being prepended/appended, the target tab // must be provided var target = null; var $targetLiTag = null; if (message.target !== null) { target = getTargetTabs($tabset, $tabContent, message.target); $targetLiTag = target.$liTag; } // If the item is to be placed inside a navbarMenu (dropdown), // change the value of $tabset from the parent's ul tag to the // dropdown's ul tag var dropdown = getDropdown(); if (dropdown !== null) { if ($aTag.attr("data-toggle") === "dropdown") throw "Cannot insert a navbarMenu inside another one"; $tabset = dropdown.$tabset; tabsetId = dropdown.id; } // For regular tab items, fix the href (of the li > a tag) // and the id (of the div tag). This does not apply to plain // text items (which function as dividers and headers inside // navbarMenus) and whole navbarMenus (since those get // constructed from scratch on the R side and therefore // there are no ids that need matching) if ($aTag.attr("data-toggle") === "tab") { var index = getTabIndex($tabset, tabsetId); var tabId = "tab-" + tabsetId + "-" + index; $liTag.find("> a").attr("href", "#" + tabId); $divTag.attr("id", tabId); } // actually insert the item into the right place if (message.position === "before") { if ($targetLiTag) { $targetLiTag.before($liTag); } else { $tabset.append($liTag); } } else if (message.position === "after") { if ($targetLiTag) { $targetLiTag.after($liTag); } else { $tabset.prepend($liTag); } } exports.renderContent($liTag[0], { html: $liTag.html(), deps: message.liTag.deps }); // jcheng 2017-07-28: This next part might look a little insane versus the // more obvious `$tabContent.append($divTag);`, but there's a method to the // madness. // // 1) We need to load the dependencies, and this needs to happen before // any scripts in $divTag get a chance to run. // 2) The scripts in $divTag need to run only once. // 3) The contents of $divTag need to be sent through renderContent so that // singletons may be registered and/or obeyed, and so that inputs/outputs // may be bound. // // Add to these constraints these facts: // // A) The (non-jQuery) DOM manipulation functions don't cause scripts to // run, but the jQuery functions all do. // B) renderContent must be called on an element that's attached to the // document. // C) $divTag may be of length > 1 (e.g. navbarMenu). I also noticed text // elements consisting of just "\n" being included in the nodeset of // $divTag. // D) renderContent has a bug where only position "replace" (the default) // uses the jQuery functions, so other positions like "beforeend" will // prevent child script tags from running. // // In theory the same problem exists for $liTag but since that content is // much less likely to include arbitrary scripts, we're skipping it. // // This code could be nicer if we didn't use renderContent, but rather the // lower-level functions that renderContent uses. Like if we pre-process // the value of message.divTag.html for singletons, we could do that, then // render dependencies, then do $tabContent.append($divTag). exports.renderContent($tabContent[0], { html: "", deps: message.divTag.deps }, "beforeend"); $divTag.get().forEach(function (el) { // Must not use jQuery for appending el to the doc, we don't want any // scripts to run (since they will run when renderContent takes a crack). $tabContent[0].appendChild(el); // If `el` itself is a script tag, this approach won't work (the script // won't be run), since we're only sending innerHTML through renderContent // and not the whole tag. That's fine in this case because we control the // R code that generates this HTML, and we know that the element is not // a script tag. exports.renderContent(el, el.innerHTML || el.textContent); }); if (message.select) { $liTag.find("a").tab("show"); } /* Barbara -- August 2017 Note: until now, the number of tabs in a tabsetPanel (or navbarPage or navlistPanel) was always fixed. So, an easy way to give an id to a tab was simply incrementing a counter. (Just like it was easy to give a random 4-digit number to identify the tabsetPanel). Now that we're introducing dynamic tabs, we must retrieve these numbers and fix the dummy id given to the tab in the R side -- there, we always set the tab id (counter dummy) to "id" and the tabset id to "tsid") */ function getTabIndex($tabset, tabsetId) { // The 0 is to ensure this works for empty tabsetPanels as well var existingTabIds = [0]; var leadingHref = "#tab-" + tabsetId + "-"; // loop through all existing tabs, find the one with highest id // (since this is based on a numeric counter), and increment $tabset.find("> li").each(function () { var $tab = $(this).find("> a[data-toggle='tab']"); if ($tab.length > 0) { var index = $tab.attr("href").replace(leadingHref, ""); existingTabIds.push(Number(index)); } }); return Math.max.apply(null, existingTabIds) + 1; } // Finds out if the item will be placed inside a navbarMenu // (dropdown). If so, returns the dropdown tabset (ul tag) // and the dropdown tabsetid (to be used to fix the tab ID) function getDropdown() { if (message.menuName !== null) { // menuName is only provided if the user wants to prepend // or append an item inside a navbarMenu (dropdown) var $dropdownATag = $("a.dropdown-toggle[data-value='" + $escape(message.menuName) + "']"); if ($dropdownATag.length === 0) { throw "There is no navbarMenu with menuName equal to '" + message.menuName + "'"; } var $dropdownTabset = $dropdownATag.find("+ ul.dropdown-menu"); var dropdownId = $dropdownTabset.attr("data-tabsetid"); return { $tabset: $dropdownTabset, id: dropdownId }; } else if (message.target !== null) { // if our item is to be placed next to a tab that is inside // a navbarMenu, our item will also be inside var $uncleTabset = $targetLiTag.parent("ul"); if ($uncleTabset.hasClass("dropdown-menu")) { var uncleId = $uncleTabset.attr("data-tabsetid"); return { $tabset: $uncleTabset, id: uncleId }; } } return null; } }); // If the given tabset has no active tabs, select the first one function ensureTabsetHasVisibleTab($tabset) { if ($tabset.find("li.active").not(".dropdown").length === 0) { // Note: destTabValue may be null. We still want to proceed // through the below logic and setValue so that the input // value for the tabset gets updated (i.e. input$tabsetId // should be null if there are no tabs). var destTabValue = getFirstTab($tabset); var inputBinding = $tabset.data('shiny-input-binding'); var evt = jQuery.Event('shiny:updateinput'); evt.binding = inputBinding; $tabset.trigger(evt); inputBinding.setValue($tabset[0], destTabValue); } } // Given a tabset ul jquery object, return the value of the first tab // (in document order) that's visible and able to be selected. function getFirstTab($ul) { return $ul.find("li:visible a[data-toggle='tab']").first().attr("data-value") || null; } function tabApplyFunction(target, func) { var liTags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; $.each(target, function (key, el) { if (key === "$liTag") { // $liTag is always just one jQuery element func(el); } else if (key === "$divTags") { // $divTags is always an array (even if length = 1) $.each(el, function (i, div) { func(div); }); } else if (liTags && key === "$liTags") { // $liTags is always an array (even if length = 0) $.each(el, function (i, div) { func(div); }); } }); } addMessageHandler("shiny-remove-tab", function (message) { var $tabset = getTabset(message.inputId); var $tabContent = getTabContent($tabset); var target = getTargetTabs($tabset, $tabContent, message.target); tabApplyFunction(target, removeEl); ensureTabsetHasVisibleTab($tabset); function removeEl($el) { exports.unbindAll($el, true); $el.remove(); } }); addMessageHandler("shiny-change-tab-visibility", function (message) { var $tabset = getTabset(message.inputId); var $tabContent = getTabContent($tabset); var target = getTargetTabs($tabset, $tabContent, message.target); tabApplyFunction(target, changeVisibility, true); ensureTabsetHasVisibleTab($tabset); function changeVisibility($el) { if (message.type === "show") $el.css("display", "");else if (message.type === "hide") { $el.hide(); $el.removeClass("active"); } } }); addMessageHandler('updateQueryString', function (message) { // leave the bookmarking code intact if (message.mode === "replace") { window.history.replaceState(null, null, message.queryString); return; } var what = null; if (message.queryString.charAt(0) === "#") what = "hash";else if (message.queryString.charAt(0) === "?") what = "query";else throw "The 'query' string must start with either '?' " + "(to update the query string) or with '#' (to " + "update the hash)."; var path = window.location.pathname; var oldQS = window.location.search; var oldHash = window.location.hash; /* Barbara -- December 2016 Note: we could check if the new QS and/or hash are different from the old one(s) and, if not, we could choose not to push a new state (whether or not we would replace it is moot/ inconsequential). However, I think that it is better to interpret each call to `updateQueryString` as representing new state (even if the message.queryString is the same), so that check isn't even performed as of right now. */ var relURL = path; if (what === "query") relURL += message.queryString;else relURL += oldQS + message.queryString; // leave old QS if it exists window.history.pushState(null, null, relURL); // for the case when message.queryString has both a query string // and a hash (`what = "hash"` allows us to trigger the // hashchange event) if (message.queryString.indexOf("#") !== -1) what = "hash"; // for the case when there was a hash before, but there isn't // any hash now (e.g. for when only the query string is updated) if (window.location.hash !== oldHash) what = "hash"; // This event needs to be triggered manually because pushState() never // causes a hashchange event to be fired, if (what === "hash") $(document).trigger("hashchange"); }); 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.el).trigger({ type: 'shiny:outputinvalidated', binding: binding, name: key }); if (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: "