EVOLUTION-MANAGER
Edit File: shiny.js
"use strict"; function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _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.5.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 (".concat(expr, ");\n } catch (e) {\n console.error('Error evaluating expression: ").concat(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() { if (arguments.length < 2) throw new Error("equal requires at least two arguments."); for (var i = 0; i < arguments.length - 1; i++) { if (!_equal(i < 0 || arguments.length <= i ? undefined : arguments[i], i + 1 < 0 || arguments.length <= i + 1 ? undefined : arguments[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: ".concat(op); }; function updateLabel(labelTxt, labelNode) { // Only update if label was specified in the update method if (typeof labelTxt === "undefined") return; if (labelNode.length !== 1) { throw new Error("labelNode must be of length 1"); } // Should the label be empty? var emptyLabel = $.isArray(labelTxt) && labelTxt.length === 0; if (emptyLabel) { labelNode.addClass("shiny-label-null"); } else { labelNode.text(labelTxt); labelNode.removeClass("shiny-label-null"); } } //--------------------------------------------------------------------- // 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 (nameType, value, opts) { this.pendingData[nameType] = 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 (nameType, value, opts) { var _splitInputNameType = splitInputNameType(nameType), inputName = _splitInputNameType.name, 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(nameType, 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), name = _splitInputNameType2.name, 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 (nameType, value, opts) { var evt = jQuery.Event("shiny:inputchanged"); var input = splitInputNameType(nameType); evt.name = input.name; evt.inputType = input.inputType; evt.value = value; evt.binding = opts.binding; evt.el = opts.el; evt.priority = opts.priority; $(opts.el).trigger(evt); if (!evt.isDefaultPrevented()) { var 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 () { // Note that the first argument of setInput() and setRatePolicy() // are passed both the input name (i.e., inputId) and type. // https://github.com/rstudio/shiny/blob/67d3a/srcjs/init_shiny.js#L111-L126 // However, $ensureInit() and $doSetInput() are meant to be passed just // the input name (i.e., inputId), which is why we distinguish between // nameType and name. this.setInput = function (nameType, value, opts) { var _splitInputNameType3 = splitInputNameType(nameType), inputName = _splitInputNameType3.name; this.$ensureInit(inputName); if (opts.priority !== "deferred") this.inputRatePolicies[inputName].immediateCall(nameType, value, opts);else this.inputRatePolicies[inputName].normalCall(nameType, value, opts); }; this.setRatePolicy = function (nameType, mode, millis) { var _splitInputNameType4 = splitInputNameType(nameType), inputName = _splitInputNameType4.name; if (mode === 'direct') { this.inputRatePolicies[inputName] = new Invoker(this, this.$doSetInput); } else if (mode === 'debounce') { this.inputRatePolicies[inputName] = new Debouncer(this, this.$doSetInput, millis); } else if (mode === 'throttle') { this.inputRatePolicies[inputName] = new Throttler(this, this.$doSetInput, millis); } }; this.$ensureInit = function (name) { if (!(name in this.inputRatePolicies)) this.setRatePolicy(name, 'direct'); }; this.$doSetInput = function (nameType, value, opts) { this.target.setInput(nameType, value, opts); }; }).call(InputRateDecorator.prototype); var InputDeferDecorator = function InputDeferDecorator(target) { this.target = target; this.pendingInput = {}; }; (function () { this.setInput = function (nameType, value, opts) { if (/^\./.test(nameType)) this.target.setInput(nameType, value, opts);else this.pendingInput[nameType] = { value: value, opts: opts }; }; this.submit = function () { for (var nameType in this.pendingInput) { if (this.pendingInput.hasOwnProperty(nameType)) { var _this$pendingInput$na = this.pendingInput[nameType], value = _this$pendingInput$na.value, opts = _this$pendingInput$na.opts; this.target.setInput(nameType, value, opts); } } }; }).call(InputDeferDecorator.prototype); var InputValidateDecorator = function InputValidateDecorator(target) { this.target = target; }; (function () { this.setInput = function (nameType, value, opts) { if (!nameType) throw "Can't set input with empty name."; opts = addDefaultInputOpts(opts); this.target.setInput(nameType, 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(nameType) { var name2 = nameType.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('<div id="shiny-disconnected-overlay"></div>'); } // 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 <head> 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]; // 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) { // remove leading url if it exists. (copy of bootstrap url stripper) var href = $tab.attr("href").replace(/.*(?=#[^\s]+$)/, ''); // remove tab id to get the index var index = href.replace("#tab-" + tabsetId + "-", ""); 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: "<div id=\"shiny-progress-".concat(message.id, "\" class=\"shiny-progress-notification\">") + '<div class="progress active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>', 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 = $('<div class="shiny-progress-container"></div>'); $(document.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 = $('<div class="shiny-progress open">' + '<div class="progress active"><div class="progress-bar bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span>' + '<span class="progress-detail"></span>' + '</div>' + '</div>'); $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' && message.value !== null) { $progress.find('.progress').show(); $progress.find('.progress-bar').width(message.value * 100 + '%'); } } 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' && message.value !== null) { $progress.find('.progress').show(); $progress.find('.bar').width(message.value * 100 + '%'); } $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 _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref2$fullUrl = _ref2.fullUrl, fullUrl = _ref2$fullUrl === void 0 ? true : _ref2$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 = '<span id="shiny-reconnect-text">Attempting to reconnect</span>' + '<span id="shiny-reconnect-time"></span>'; var action = '<a id="shiny-reconnect-now" href="#" onclick="Shiny.shinyapp.reconnect();">Try now</a>'; 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 _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref3$html = _ref3.html, html = _ref3$html === void 0 ? '' : _ref3$html, _ref3$action = _ref3.action, action = _ref3$action === void 0 ? '' : _ref3$action, _ref3$deps = _ref3.deps, deps = _ref3$deps === void 0 ? [] : _ref3$deps, _ref3$duration = _ref3.duration, duration = _ref3$duration === void 0 ? 5000 : _ref3$duration, _ref3$id = _ref3.id, id = _ref3$id === void 0 ? null : _ref3$id, _ref3$closeButton = _ref3.closeButton, closeButton = _ref3$closeButton === void 0 ? true : _ref3$closeButton, _ref3$type = _ref3.type, type = _ref3$type === void 0 ? null : _ref3$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 = "<div class=\"shiny-notification-content-text\">".concat(html, "</div>") + "<div class=\"shiny-notification-content-action\">".concat(action, "</div>"); 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('<div class="shiny-notification-close">×</div>'); } 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; $(document.body).append('<div id="shiny-notification-panel">'); 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 = $("<div id=\"shiny-notification-".concat(id, "\" class=\"shiny-notification\">") + '<div class="shiny-notification-close">×</div>' + '<div class="shiny-notification-content"></div>' + '</div>'); $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 _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref4$html = _ref4.html, html = _ref4$html === void 0 ? '' : _ref4$html, _ref4$deps = _ref4.deps, deps = _ref4$deps === void 0 ? [] : _ref4$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 = $('<div id="shiny-modal-wrapper"></div>'); $(document.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; // 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; } // this checks only against base64 encoded src values // images put here are only from renderImage and renderPlot if (key === "src" && value === img.getAttribute("src")) { // Ensure the browser actually fires an onLoad event, which doesn't // happen on WebKit if the value we set on src is the same as the // value it already has // https://github.com/rstudio/shiny/issues/2197 // https://stackoverflow.com/questions/5024111/javascript-image-onload-doesnt-fire-in-webkit-if-loading-same-image img.removeAttribute("src"); } 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 = { panels: [], dims: { // These values be set to the naturalWidth and naturalHeight once the image has loaded height: null, width: null } }; } // Remove event handlers that were added in previous runs of this function. $el.off('.image_output'); $img.off('.image_output'); // When the image loads, initialize all the interaction handlers. When the // value of src is set, the browser may not load the image immediately, // even if it's a data URL. If we try to initialize this stuff // immediately, it can cause problems because we use we need the raw image // height and width $img.off("load.shiny_image_interaction"); $img.one("load.shiny_image_interaction", function () { 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) { imageutils.disableDrag($el, $img); var clickHandler = imageutils.createClickHandler(opts.clickId, opts.clickClip, opts.coordmap); $el.on('mousedown2.image_output', clickHandler.mousedown); $el.on('resize.image_output', clickHandler.onResize); // When img is reset, do housekeeping: clear $el's mouse listener and // call the handler's onResetImg callback. $img.on('reset.image_output', clickHandler.onResetImg); } if (opts.dblclickId) { imageutils.disableDrag($el, $img); // 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); $el.on('resize.image_output', dblclickHandler.onResize); $img.on('reset.image_output', dblclickHandler.onResetImg); } if (opts.hoverId) { imageutils.disableDrag($el, $img); 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); $el.on('resize.image_output', hoverHandler.onResize); $img.on('reset.image_output', hoverHandler.onResetImg); } if (opts.brushId) { imageutils.disableDrag($el, $img); 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); $el.on('resize.image_output', brushHandler.onResize); $img.on('reset.image_output', 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); }, resize: function resize(el, width, height) { $(el).find("img").trigger("resize"); } }); outputBindings.register(imageOutputBinding, 'shiny.imageOutput'); var imageutils = {}; imageutils.disableDrag = function ($el, $img) { // Make image non-draggable (Chrome, Safari) $img.css('-webkit-user-drag', 'none'); // Firefox, IE<=10 // First remove existing handler so we don't keep adding handlers. $img.off('dragstart.image_output'); $img.on('dragstart.image_output', function () { return false; }); // Disable selection of image and text when dragging in IE<=10 $el.off('selectstart.image_output'); $el.on('selectstart.image_output', function () { return false; }); }; // Modifies the panel objects in a coordmap, adding scaleImgToData(), // scaleDataToImg(), and clipImg() functions to each one. The panel objects // use img and data coordinates only; they do not use css coordinates. The // domain is in data coordinates; the range is in img coordinates. imageutils.initPanelScales = function (panels) { // 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); // Given an object of form {x:1, y:2}, or {x:1, xmin:2:, ymax: 3}, convert // from data coordinates to img. Whether a value is converted as x or y // depends on the first character of the key. panel.scaleDataToImg = function (val, clip) { return mapValues(val, function (value, key) { var prefix = key.substring(0, 1); if (prefix === "x") { return xscaler.scale(value, clip); } else if (prefix === "y") { return yscaler.scale(value, clip); } return null; }); }; panel.scaleImgToData = function (val, clip) { return mapValues(val, function (value, key) { var prefix = key.substring(0, 1); if (prefix === "x") { return xscaler.scaleInv(value, clip); } else if (prefix === "y") { return yscaler.scaleInv(value, clip); } return null; }); }; // Given a scaled offset (in img pixels), clip it to the nearest panel region. panel.clipImg = function (offset_img) { var newOffset = { x: offset_img.x, y: offset_img.y }; var bounds = panel.range; if (offset_img.x > bounds.right) newOffset.x = bounds.right;else if (offset_img.x < bounds.left) newOffset.x = bounds.left; if (offset_img.y > bounds.bottom) newOffset.y = bounds.bottom;else if (offset_img.y < bounds.top) newOffset.y = bounds.top; return newOffset; }; } // Add the functions to each panel object. for (var i = 0; i < panels.length; i++) { var panel = panels[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 scaleImgToData, scaleDataToImg, // and clip functions. // // There are three coordinate spaces which we need to translate between: // // 1. css: The pixel coordinates in the web browser, also known as CSS pixels. // The origin is the upper-left corner of the <img> (not including padding // and border). // 2. img: The pixel coordinates of the image data. A common case is on a // HiDPI device, where the source PNG image could be 1000 pixels wide but // be displayed in 500 CSS pixels. Another case is when the image has // additional scaling due to CSS transforms or width. // 3. data: The coordinates in the data space. This is a bit more complicated // than the other two, because there can be multiple panels (as in facets). imageutils.initCoordmap = function ($el, coordmap) { var $img = $el.find("img"); var img = $img[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.panels.length === 0) { var bounds = { top: 0, left: 0, right: img.clientWidth - 1, bottom: img.clientHeight - 1 }; coordmap.panels[0] = { domain: bounds, range: bounds, mapping: {} }; } // If no dim height and width values are found, set them to the raw image height and width // These values should be the same... // This is only done to initialize an image output, whose height and width are unknown until the image is retrieved coordmap.dims.height = coordmap.dims.height || img.naturalHeight; coordmap.dims.width = coordmap.dims.width || img.naturalWidth; // Add scaling functions to each panel imageutils.initPanelScales(coordmap.panels); // This returns the offset of the mouse in CSS pixels relative to the img, // but not including the padding or border, if present. coordmap.mouseOffsetCss = function (mouseEvent) { var img_origin = findOrigin($img); // The offset of the mouse from the upper-left corner of the img, in // pixels. return { x: mouseEvent.pageX - img_origin.x, y: mouseEvent.pageY - img_origin.y }; }; // Given an offset in an img in CSS pixels, return the corresponding offset // in source image pixels. The offset_css can have properties like "x", // "xmin", "y", and "ymax" -- anything that starts with "x" and "y". If the // img content is 1000 pixels wide, but is scaled to 400 pixels on screen, // and the input is x:400, then this will return x:1000. coordmap.scaleCssToImg = function (offset_css) { var pixel_scaling = coordmap.imgToCssScalingRatio(); var result = mapValues(offset_css, function (value, key) { var prefix = key.substring(0, 1); if (prefix === "x") { return offset_css[key] / pixel_scaling.x; } else if (prefix === "y") { return offset_css[key] / pixel_scaling.y; } return null; }); return result; }; // Given an offset in an img, in source image pixels, return the // corresponding offset in CSS pixels. If the img content is 1000 pixels // wide, but is scaled to 400 pixels on screen, and the input is x:1000, // then this will return x:400. coordmap.scaleImgToCss = function (offset_img) { var pixel_scaling = coordmap.imgToCssScalingRatio(); var result = mapValues(offset_img, function (value, key) { var prefix = key.substring(0, 1); if (prefix === "x") { return offset_img[key] * pixel_scaling.x; } else if (prefix === "y") { return offset_img[key] * pixel_scaling.y; } return null; }); return result; }; // Returns the x and y ratio the image content is scaled to on screen. If // the image data is 1000 pixels wide and is scaled to 300 pixels on screen, // then this returns 0.3. (Note the 300 pixels refers to CSS pixels.) coordmap.imgToCssScalingRatio = function () { var img_dims = findDims($img); return { x: img_dims.x / coordmap.dims.width, y: img_dims.y / coordmap.dims.height }; }; coordmap.cssToImgScalingRatio = function () { var res = coordmap.imgToCssScalingRatio(); return { x: 1 / res.x, y: 1 / res.y }; }; // Given an offset in css pixels, 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.getPanelCss = function (offset_css) { var expand = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var offset_img = coordmap.scaleCssToImg(offset_css); var x = offset_img.x; var y = offset_img.y; // Convert expand from css pixels to img pixels var cssToImgRatio = coordmap.cssToImgScalingRatio(); var expand_img = { x: expand * cssToImgRatio.x, y: expand * cssToImgRatio.y }; var matches = []; // Panels that match var dists = []; // Distance of offset to each matching panel var b; for (var i = 0; i < coordmap.panels.length; i++) { b = coordmap.panels[i].range; if (x <= b.right + expand_img.x && x >= b.left - expand_img.x && y <= b.bottom + expand_img.y && y >= b.top - expand_img.y) { matches.push(coordmap.panels[i]); // Find distance from edges for x and y var xdist = 0; var ydist = 0; if (x > b.right && x <= b.right + expand_img.x) { xdist = x - b.right; } else if (x < b.left && x >= b.left - expand_img.x) { xdist = x - b.left; } if (y > b.bottom && y <= b.bottom + expand_img.y) { ydist = y - b.bottom; } else if (y < b.top && y >= b.top - expand_img.y) { 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 css pixels) in a panel? If supplied, `expand` tells us // to expand the panels by that many pixels in all directions. coordmap.isInPanelCss = function (offset_css) { var expand = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if (coordmap.getPanelCss(offset_css, 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.setInputValue(inputId, null); return; } var coords = {}; var coords_css = coordmap.mouseOffsetCss(e); // If outside of plotting region if (!coordmap.isInPanelCss(coords_css)) { if (nullOutside) { exports.setInputValue(inputId, null); return; } if (clip) return; coords.coords_css = coords_css; coords.coords_img = coordmap.scaleCssToImg(coords_css); exports.setInputValue(inputId, coords, { priority: "event" }); return; } var panel = coordmap.getPanelCss(coords_css); var coords_img = coordmap.scaleCssToImg(coords_css); var coords_data = panel.scaleImgToData(coords_img); coords.x = coords_data.x; coords.y = coords_data.y; coords.coords_css = coords_css; coords.coords_img = coords_img; coords.img_css_ratio = coordmap.cssToImgScalingRatio(); // 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; exports.setInputValue(inputId, coords, { priority: "event" }); }; }; }; // 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). imageutils.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. imageutils.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; }; // 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 }); $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.pageX - e.pageX) > 2 || Math.abs(pending_e.pageY - e.pageY) > 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); }, onResize: 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); }, onResize: 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.setInputValue(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); coords.coords_css = brush.boundsCss(); coords.coords_img = coordmap.scaleCssToImg(coords.coords_css); coords.img_css_ratio = coordmap.cssToImgScalingRatio(); // 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.setInputValue(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; // In general, brush uses css pixels, and coordmap uses img pixels. var offset_css = coordmap.mouseOffsetCss(e); // Ignore mousedown events outside of plotting region, expanded by // a number of pixels specified in expandPixels. if (opts.brushClip && !coordmap.isInPanelCss(offset_css, expandPixels)) return; brush.up({ x: NaN, y: NaN }); brush.down(offset_css); if (brush.isInResizeArea(offset_css)) { brush.startResizing(offset_css); // 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_css)) { brush.startDragging(offset_css); 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.getPanelCss(offset_css, expandPixels); brush.startBrushing(panel.clipImg(coordmap.scaleCssToImg(offset_css))); // 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) { // In general, brush uses css pixels, and coordmap uses img pixels. var offset_css = coordmap.mouseOffsetCss(e); if (!(brush.isBrushing() || brush.isDragging() || brush.isResizing())) { // Set the cursor depending on where it is if (brush.isInResizeArea(offset_css)) { var r = brush.whichResizeSides(offset_css); 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_css)) { setCursorStyle('grabbable'); } else if (coordmap.isInPanelCss(offset_css, expandPixels)) { setCursorStyle('crosshair'); } else { setCursorStyle(null); } } } // mousemove handlers while brushing or dragging function mousemoveBrushing(e) { brush.brushTo(coordmap.mouseOffsetCss(e)); brushInfoSender.normalCall(); } function mousemoveDragging(e) { brush.dragTo(coordmap.mouseOffsetCss(e)); brushInfoSender.normalCall(); } function mousemoveResizing(e) { brush.resizeTo(coordmap.mouseOffsetCss(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.mouseOffsetCss(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.mouseOffsetCss(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.mouseOffsetCss(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")) { // Importing an old brush must happen after the image data has loaded // and the <img> DOM element has the updated size. If importOldBrush() // is called before this happens, then the css-img coordinate mappings // will give the wrong result, and the brush will have the wrong // position. // // jcheng 09/26/2018: This used to happen in img.onLoad, but recently // we moved to all brush initialization moving to img.onLoad so this // logic can be executed inline. brush.importOldBrush(); brushInfoSender.immediateCall(); } } function onResize() { brush.onResize(); brushInfoSender.immediateCall(); } return { mousedown: mousedown, mousemove: mousemove, onResetImg: onResetImg, onResize: onResize }; }; // 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 = {}; // Aliases for conciseness var cssToImg = coordmap.scaleCssToImg; var imgToCss = coordmap.scaleImgToCss; 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 (in CSS pixels) 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 CSS 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.boundsCss = { 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 (in CSS pixels) 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; // 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.panels.length; i++) { var curPanel = coordmap.panels[i]; if (equal(oldPanel.mapping, curPanel.mapping) && equal(oldPanel.panel_vars, curPanel.panel_vars)) { // We've found a matching panel state.panel = coordmap.panels[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(); } // This will reposition the brush div when the image is resized, maintaining // the same data coordinates. Note that the "resize" here refers to the // wrapper div/img being resized; elsewhere, "resize" refers to the brush // div being resized. function onResize() { var bounds_data = boundsData(); // Check to see if we have valid boundsData for (var val in bounds_data) { if (isnan(bounds_data[val])) return; } boundsData(bounds_data); updateDiv(); } // Return true if the offset is inside min/max coords function isInsideBrush(offset_css) { var bounds = state.boundsCss; return offset_css.x <= bounds.xmax && offset_css.x >= bounds.xmin && offset_css.y <= bounds.ymax && offset_css.y >= bounds.ymin; } // Return true if offset is inside a region to start a resize function isInResizeArea(offset_css) { var sides = whichResizeSides(offset_css); return sides.left || sides.right || sides.top || sides.bottom; } // Return an object representing which resize region(s) the cursor is in. function whichResizeSides(offset_css) { var b = state.boundsCss; // 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_css.y <= e.ymax && offset_css.y >= e.ymin) { if (offset_css.x < b.xmin && offset_css.x >= e.xmin) res.left = true;else if (offset_css.x > b.xmax && offset_css.x <= e.xmax) res.right = true; } if ((opts.brushDirection === 'xy' || opts.brushDirection === 'y') && offset_css.x <= e.xmax && offset_css.x >= e.xmin) { if (offset_css.y < b.ymin && offset_css.y >= e.ymin) res.top = true;else if (offset_css.y > b.ymax && offset_css.y <= e.ymax) res.bottom = true; } return res; } // Sets the bounds of the brush (in CSS pixels), 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 boundsCss(box_css) { if (box_css === undefined) { return $.extend({}, state.boundsCss); } var min_css = { x: box_css.xmin, y: box_css.ymin }; var max_css = { x: box_css.xmax, y: box_css.ymax }; var panel = state.panel; var panelBounds_img = panel.range; if (opts.brushClip) { min_css = imgToCss(panel.clipImg(cssToImg(min_css))); max_css = imgToCss(panel.clipImg(cssToImg(max_css))); } if (opts.brushDirection === 'xy') {// No change } else if (opts.brushDirection === 'x') { // Extend top and bottom of plotting area min_css.y = imgToCss({ y: panelBounds_img.top }).y; max_css.y = imgToCss({ y: panelBounds_img.bottom }).y; } else if (opts.brushDirection === 'y') { min_css.x = imgToCss({ x: panelBounds_img.left }).x; max_css.x = imgToCss({ x: panelBounds_img.right }).x; } state.boundsCss = { xmin: min_css.x, xmax: max_css.x, ymin: min_css.y, ymax: max_css.y }; // Positions in data space var min_data = state.panel.scaleImgToData(cssToImg(min_css)); var max_data = state.panel.scaleImgToData(cssToImg(max_css)); // For reversed scales, the min and max can be reversed, so use findBox // to ensure correct order. state.boundsData = imageutils.findBox(min_data, max_data); // Round to 14 significant digits to avoid spurious changes in FP values // (#1634). state.boundsData = mapValues(state.boundsData, function (val) { return roundSignif(val, 14); }); // 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_data) { if (box_data === undefined) { return $.extend({}, state.boundsData); } var box_css = imgToCss(state.panel.scaleDataToImg(box_data)); // Round to 13 significant digits to avoid spurious changes in FP values // (#2197). box_css = mapValues(box_css, function (val) { return roundSignif(val, 13); }); // The scaling function can reverse the direction of the axes, so we need to // find the min and max again. boundsCss({ xmin: Math.min(box_css.xmin, box_css.xmax), xmax: Math.max(box_css.xmin, box_css.xmax), ymin: Math.min(box_css.ymin, box_css.ymax), ymax: Math.max(box_css.ymin, box_css.ymax) }); 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 img_offset_css = findOrigin($el.find("img")); var b = state.boundsCss; $div.offset({ top: img_offset_css.y + b.ymin, left: img_offset_css.x + b.xmin }).outerWidth(b.xmax - b.xmin + 1).outerHeight(b.ymax - b.ymin + 1); } function down(offset_css) { if (offset_css === undefined) return state.down; state.down = offset_css; return undefined; } function up(offset_css) { if (offset_css === undefined) return state.up; state.up = offset_css; return undefined; } function isBrushing() { return state.brushing; } function startBrushing() { state.brushing = true; addDiv(); state.panel = coordmap.getPanelCss(state.down, expandPixels); boundsCss(imageutils.findBox(state.down, state.down)); updateDiv(); } function brushTo(offset_css) { boundsCss(imageutils.findBox(state.down, offset_css)); $div.show(); updateDiv(); } function stopBrushing() { state.brushing = false; // Save the final bounding box of the brush boundsCss(imageutils.findBox(state.down, state.up)); } function isDragging() { return state.dragging; } function startDragging() { state.dragging = true; state.changeStartBounds = $.extend({}, state.boundsCss); } function dragTo(offset_css) { // How far the brush was dragged var dx = offset_css.x - state.down.x; var dy = offset_css.y - state.down.y; // Calculate what new positions would be, before clipping. var start = state.changeStartBounds; var newBounds_css = { 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_img = state.panel.range; var newBounds_img = cssToImg(newBounds_css); // Convert to format for shiftToRange var xvals_img = [newBounds_img.xmin, newBounds_img.xmax]; var yvals_img = [newBounds_img.ymin, newBounds_img.ymax]; xvals_img = imageutils.shiftToRange(xvals_img, panelBounds_img.left, panelBounds_img.right); yvals_img = imageutils.shiftToRange(yvals_img, panelBounds_img.top, panelBounds_img.bottom); // Convert back to bounds format newBounds_css = imgToCss({ xmin: xvals_img[0], xmax: xvals_img[1], ymin: yvals_img[0], ymax: yvals_img[1] }); } boundsCss(newBounds_css); updateDiv(); } function stopDragging() { state.dragging = false; } function isResizing() { return state.resizing; } function startResizing() { state.resizing = true; state.changeStartBounds = $.extend({}, state.boundsCss); state.resizeSides = whichResizeSides(state.down); } function resizeTo(offset_css) { // How far the brush was dragged var d_css = { x: offset_css.x - state.down.x, y: offset_css.y - state.down.y }; var d_img = cssToImg(d_css); // Calculate what new positions would be, before clipping. var b_img = cssToImg(state.changeStartBounds); var panelBounds_img = state.panel.range; if (state.resizeSides.left) { var xmin_img = imageutils.shiftToRange(b_img.xmin + d_img.x, panelBounds_img.left, b_img.xmax)[0]; b_img.xmin = xmin_img; } else if (state.resizeSides.right) { var xmax_img = imageutils.shiftToRange(b_img.xmax + d_img.x, b_img.xmin, panelBounds_img.right)[0]; b_img.xmax = xmax_img; } if (state.resizeSides.top) { var ymin_img = imageutils.shiftToRange(b_img.ymin + d_img.y, panelBounds_img.top, b_img.ymax)[0]; b_img.ymin = ymin_img; } else if (state.resizeSides.bottom) { var ymax_img = imageutils.shiftToRange(b_img.ymax + d_img.y, b_img.ymin, panelBounds_img.bottom)[0]; b_img.ymax = ymax_img; } boundsCss(imgToCss(b_img)); updateDiv(); } function stopResizing() { state.resizing = false; } return { reset: reset, importOldBrush: importOldBrush, isInsideBrush: isInsideBrush, isInResizeArea: isInResizeArea, whichResizeSides: whichResizeSides, onResize: onResize, // A callback when the wrapper div or img is resized. boundsCss: boundsCss, 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.setInputValue(brushId, null); imageOutputBinding.find(document).trigger("shiny-internal:brushed", { brushId: brushId, outputId: null }); }; // ----------------------------------------------------------------------- // Utility functions for finding dimensions and locations of DOM elements // ----------------------------------------------------------------------- // Returns the ratio that an element has been scaled (for example, by CSS // transforms) in the x and y directions. function findScalingRatio($el) { var boundingRect = $el[0].getBoundingClientRect(); return { x: boundingRect.width / $el.outerWidth(), y: boundingRect.height / $el.outerHeight() }; } function findOrigin($el) { var offset = $el.offset(); var scaling_ratio = findScalingRatio($el); // Find the size of the padding and border, for the top and left. This is // before any transforms. var paddingBorder = { left: parseInt($el.css("border-left-width")) + parseInt($el.css("padding-left")), top: parseInt($el.css("border-top-width")) + parseInt($el.css("padding-top")) }; // offset() returns the upper left corner of the element relative to the // page, but it includes padding and border. Here we find the upper left // of the element, not including padding and border. return { x: offset.left + scaling_ratio.x * paddingBorder.left, y: offset.top + scaling_ratio.y * paddingBorder.top }; } // Find the dimensions of a tag, after transforms, and without padding and // border. function findDims($el) { // If there's any padding/border, we need to find the ratio of the actual // element content compared to the element plus padding and border. var content_ratio = { x: $el.width() / $el.outerWidth(), y: $el.height() / $el.outerHeight() }; // Get the dimensions of the element _after_ any CSS transforms. This // includes the padding and border. var bounding_rect = $el[0].getBoundingClientRect(); // Dimensions of the element after any CSS transforms, and without // padding/border. return { x: content_ratio.x * bounding_rect.width, y: content_ratio.y * bounding_rect.height }; } //--------------------------------------------------------------------- // 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); } var html; var dependencies = []; if (content === null) { html = ''; } else if (typeof content === 'string') { html = content; } else if (_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 $("<meta>").attr("name", name).attr("content", obj[name]); }); $head.append(metas); } if (dep.stylesheet) { var stylesheets = $.map(asArray(dep.stylesheet), function (stylesheet) { return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet)); }); $head.append(stylesheets); } if (dep.script) { var scripts = $.map(asArray(dep.script), function (scriptName) { return $("<script>").attr("src", href + "/" + encodeURI(scriptName)); }); $head.append(scripts); } if (dep.attachment) { // dep.attachment might be a single string, an array, or an object. var attachments = dep.attachment; if (typeof attachments === "string") attachments = [attachments]; if ($.isArray(attachments)) { // The contract for attachments is that arrays of attachments are // addressed using 1-based indexes. Convert this array to an object. var tmp = {}; $.each(attachments, function (index, attachment) { tmp[index + 1 + ""] = attachment; }); attachments = tmp; } var attach = $.map(attachments, function (attachment, key) { return $("<link rel='attachment'>").attr("id", dep.name + "-" + key + "-attachment").attr("href", href + "/" + encodeURI(attachment)); }); $head.append(attach); } if (dep.head) { var $newHead = $("<head></head>"); $newHead.html(dep.head); $head.append($newHead.children()); } return true; } var singletons = { knownSingletons: {}, renderHtml: function renderHtml(html, el, where) { var processed = this._processHtml(html); this._addToHead(processed.head); this.register(processed.singletons); if (where === "replace") { $(el).html(processed.html); } else { el.insertAdjacentHTML(where, processed.html); } return processed; }, // Take an object where keys are names of singletons, and merges it into // knownSingletons register: function register(s) { $.extend(this.knownSingletons, s); }, // Takes a string or array of strings and adds them to knownSingletons registerNames: function registerNames(s) { if (typeof s === 'string') { this.knownSingletons[s] = true; } else if (s instanceof Array) { for (var i = 0; i < s.length; i++) { this.knownSingletons[s[i]] = true; } } }, // Inserts new content into document head _addToHead: function _addToHead(head) { if (head.length > 0) { var tempDiv = $("<div>" + head + "</div>")[0]; var $head = $('head'); while (tempDiv.hasChildNodes()) { $head.append(tempDiv.firstChild); } } }, // Reads HTML and returns an object with info about singletons _processHtml: function _processHtml(val) { var self = this; var newSingletons = {}; var newVal; var findNewPayload = function findNewPayload(match, p1, sig, payload) { if (self.knownSingletons[sig] || newSingletons[sig]) return ""; newSingletons[sig] = true; return payload; }; while (true) { newVal = val.replace(self._reSingleton, findNewPayload); if (val.length === newVal.length) break; val = newVal; } var heads = []; var headAddPayload = function headAddPayload(match, payload) { heads.push(payload); return ""; }; while (true) { newVal = val.replace(self._reHead, headAddPayload); if (val.length === newVal.length) break; val = newVal; } return { html: val, head: heads.join("\n"), singletons: newSingletons }; }, _reSingleton: /<!--(SHINY.SINGLETON\[([\w]+)\])-->([\s\S]*?)<!--\/\1-->/, _reHead: /<head(?:\s[^>]*)?>([\s\S]*?)<\/head>/ }; //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding_downloadlink.js var downloadLinkOutputBinding = new OutputBinding(); $.extend(downloadLinkOutputBinding, { find: function find(scope) { return $(scope).find('a.shiny-download-link'); }, renderValue: function renderValue(el, data) { $(el).attr('href', data); } }); outputBindings.register(downloadLinkOutputBinding, 'shiny.downloadLink'); // Trigger shiny:filedownload event whenever a downloadButton/Link is clicked $(document).on('click.shinyDownloadLink', 'a.shiny-download-link', function (e) { var evt = jQuery.Event('shiny:filedownload'); evt.name = this.id; evt.href = this.href; $(document).trigger(evt); }); //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding_datatable.js var datatableOutputBinding = new OutputBinding(); $.extend(datatableOutputBinding, { find: function find(scope) { return $(scope).find('.shiny-datatable-output'); }, onValueError: function onValueError(el, err) { exports.unbindAll(el); this.renderError(el, err); }, renderValue: function renderValue(el, _data) { var $el = $(el).empty(); if (!_data || !_data.colnames) return; var colnames = $.makeArray(_data.colnames); var header = $.map(colnames, function (x) { return '<th>' + x + '</th>'; }).join(''); header = '<thead><tr>' + header + '</tr></thead>'; var footer = ''; if (_data.options === null || _data.options.searching !== false) { footer = $.map(colnames, function (x) { // placeholder needs to be escaped (and HTML tags are stripped off) return '<th><input type="text" placeholder="' + escapeHTML(x.replace(/(<([^>]+)>)/ig, '')) + '" /></th>'; }).join(''); footer = '<tfoot>' + footer + '</tfoot>'; } var content = '<table class="table table-striped table-hover">' + header + footer + '</table>'; $el.append(content); // options that should be eval()ed if (_data.evalOptions) $.each(_data.evalOptions, function (i, x) { /*jshint evil: true */ _data.options[x] = eval('(' + _data.options[x] + ')'); }); // caseInsensitive searching? default true var searchCI = _data.options === null || typeof _data.options.search === 'undefined' || _data.options.search.caseInsensitive !== false; var oTable = $(el).children("table").DataTable($.extend({ "processing": true, "serverSide": true, "order": [], "orderClasses": false, "pageLength": 25, "ajax": { "url": _data.action, "type": "POST", "data": function data(d) { d.search.caseInsensitive = searchCI; d.escape = _data.escape; } } }, _data.options)); // the table object may need post-processing if (typeof _data.callback === 'string') { /*jshint evil: true */ var callback = eval('(' + _data.callback + ')'); if (typeof callback === 'function') callback(oTable); } // use debouncing for searching boxes $el.find('label input').first().unbind('keyup').keyup(debounce(_data.searchDelay, function () { oTable.search(this.value).draw(); })); var searchInputs = $el.find("tfoot input"); if (searchInputs.length > 0) { // this is a little weird: aoColumns/bSearchable are still in DT 1.10 // https://github.com/DataTables/DataTables/issues/388 $.each(oTable.settings()[0].aoColumns, function (i, x) { // hide the text box if not searchable if (!x.bSearchable) searchInputs.eq(i).hide(); }); searchInputs.keyup(debounce(_data.searchDelay, function () { oTable.column(searchInputs.index(this)).search(this.value).draw(); })); } // FIXME: ugly scrollbars in tab panels b/c Bootstrap uses 'visible: auto' $el.parents('.tab-content').css('overflow', 'visible'); } }); outputBindings.register(datatableOutputBinding, 'shiny.datatableOutput'); //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding_adapter.js var OutputBindingAdapter = function OutputBindingAdapter(el, binding) { this.el = el; this.binding = binding; // If the binding actually has a resize method, override the prototype of // onResize with a version that does a makeResizeFilter on the element. if (binding.resize) { this.onResize = makeResizeFilter(el, function (width, height) { binding.resize(el, width, height); }); } }; (function () { this.getId = function () { return this.binding.getId(this.el); }; this.onValueChange = function (data) { this.binding.onValueChange(this.el, data); }; this.onValueError = function (err) { this.binding.onValueError(this.el, err); }; this.showProgress = function (show) { this.binding.showProgress(this.el, show); }; this.onResize = function () {// Intentionally left blank; see constructor }; }).call(OutputBindingAdapter.prototype); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding.js var InputBinding = exports.InputBinding = 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; }; // Gives the input a type in case the server needs to know it // to deserialize the JSON correctly this.getType = function () { return false; }; this.getValue = function (el) { throw "Not implemented"; }; // The callback method takes one argument, whose value is boolean. If true, // allow deferred (debounce or throttle) sending depending on the value of // getRatePolicy. If false, send value immediately. this.subscribe = function (el, callback) {}; this.unsubscribe = function (el) {}; // This is used for receiving messages that tell the input object to do // things, such as setting values (including min, max, and others). // 'data' should be an object with elements corresponding to value, min, // max, etc., as appropriate for the type of input object. It also should // trigger a change event. this.receiveMessage = function (el, data) { throw "Not implemented"; }; this.getState = function (el, data) { throw "Not implemented"; }; this.getRatePolicy = function () { return null; }; // Some input objects need initialization before being bound. This is // called when the document is ready (for statically-added input objects), // and when new input objects are added to the document with // htmlOutputBinding.renderValue() (for dynamically-added input objects). // This is called before the input is bound. this.initialize = function (el) {}; // This is called after unbinding the output. this.dispose = function (el) {}; }).call(InputBinding.prototype); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_text.js var textInputBinding = new InputBinding(); $.extend(textInputBinding, { find: function find(scope) { var $inputs = $(scope).find('input[type="text"], input[type="search"], input[type="url"], input[type="email"]'); // selectize.js 0.12.4 inserts a hidden text input with an // id that ends in '-selectized'. The .not() selector below // is to prevent textInputBinding from accidentally picking up // this hidden element as a shiny input (#2396) return $inputs.not('input[type="text"][id$="-selectized"]'); }, getId: function getId(el) { return InputBinding.prototype.getId.call(this, el) || el.name; }, getValue: function getValue(el) { return el.value; }, setValue: function setValue(el, value) { el.value = value; }, subscribe: function subscribe(el, callback) { $(el).on('keyup.textInputBinding input.textInputBinding', function (event) { callback(true); }); $(el).on('change.textInputBinding', function (event) { callback(false); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.textInputBinding'); }, receiveMessage: function receiveMessage(el, data) { if (data.hasOwnProperty('value')) this.setValue(el, data.value); updateLabel(data.label, this._getLabelNode(el)); if (data.hasOwnProperty('placeholder')) el.placeholder = data.placeholder; $(el).trigger('change'); }, getState: function getState(el) { return { label: this._getLabelNode(el).text(), value: el.value, placeholder: el.placeholder }; }, getRatePolicy: function getRatePolicy() { return { policy: 'debounce', delay: 250 }; }, _getLabelNode: function _getLabelNode(el) { return $(el).parent().find('label[for="' + $escape(el.id) + '"]'); } }); inputBindings.register(textInputBinding, 'shiny.textInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_textarea.js var textareaInputBinding = {}; $.extend(textareaInputBinding, textInputBinding, { find: function find(scope) { return $(scope).find('textarea'); } }); inputBindings.register(textareaInputBinding, 'shiny.textareaInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_password.js var passwordInputBinding = {}; $.extend(passwordInputBinding, textInputBinding, { find: function find(scope) { return $(scope).find('input[type="password"]'); }, getType: function getType(el) { return "shiny.password"; } }); inputBindings.register(passwordInputBinding, 'shiny.passwordInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_number.js var numberInputBinding = {}; $.extend(numberInputBinding, textInputBinding, { find: function find(scope) { return $(scope).find('input[type="number"]'); }, getValue: function getValue(el) { var numberVal = $(el).val(); if (/^\s*$/.test(numberVal)) // Return null if all whitespace return null;else if (!isNaN(numberVal)) // If valid Javascript number string, coerce to number return +numberVal;else return numberVal; // If other string like "1e6", send it unchanged }, setValue: function setValue(el, value) { el.value = value; }, getType: function getType(el) { return "shiny.number"; }, receiveMessage: function receiveMessage(el, data) { if (data.hasOwnProperty('value')) el.value = data.value; if (data.hasOwnProperty('min')) el.min = data.min; if (data.hasOwnProperty('max')) el.max = data.max; if (data.hasOwnProperty('step')) el.step = data.step; updateLabel(data.label, this._getLabelNode(el)); $(el).trigger('change'); }, getState: function getState(el) { return { label: this._getLabelNode(el).text(), value: this.getValue(el), min: Number(el.min), max: Number(el.max), step: Number(el.step) }; }, _getLabelNode: function _getLabelNode(el) { return $(el).parent().find('label[for="' + $escape(el.id) + '"]'); } }); inputBindings.register(numberInputBinding, 'shiny.numberInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_checkbox.js var checkboxInputBinding = new InputBinding(); $.extend(checkboxInputBinding, { find: function find(scope) { return $(scope).find('input[type="checkbox"]'); }, getValue: function getValue(el) { return el.checked; }, setValue: function setValue(el, value) { el.checked = value; }, subscribe: function subscribe(el, callback) { $(el).on('change.checkboxInputBinding', function (event) { callback(true); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.checkboxInputBinding'); }, getState: function getState(el) { return { label: $(el).parent().find('span').text(), value: el.checked }; }, receiveMessage: function receiveMessage(el, data) { if (data.hasOwnProperty('value')) el.checked = data.value; // checkboxInput()'s label works different from other // input labels...the label container should always exist if (data.hasOwnProperty('label')) $(el).parent().find('span').text(data.label); $(el).trigger('change'); } }); inputBindings.register(checkboxInputBinding, 'shiny.checkboxInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_slider.js // Necessary to get hidden sliders to send their updated values function forceIonSliderUpdate(slider) { if (slider.$cache && slider.$cache.input) slider.$cache.input.trigger('change');else console.log("Couldn't force ion slider to update"); } function getTypePrettifyer(dataType, timeFormat, timezone) { var timeFormatter; var prettify; if (dataType === 'date') { timeFormatter = strftime.utc(); prettify = function prettify(num) { return timeFormatter(timeFormat, new Date(num)); }; } else if (dataType === 'datetime') { if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime; prettify = function prettify(num) { return timeFormatter(timeFormat, new Date(num)); }; } else { // The default prettify function for ion.rangeSlider adds thousands // separators after the decimal mark, so we have our own version here. // (#1958) prettify = function prettify(num) { // When executed, `this` will refer to the `IonRangeSlider.options` // object. return formatNumber(num, this.prettify_separator); }; } return prettify; } var sliderInputBinding = {}; $.extend(sliderInputBinding, textInputBinding, { find: function find(scope) { // Check if ionRangeSlider plugin is loaded if (!$.fn.ionRangeSlider) return []; return $(scope).find('input.js-range-slider'); }, getType: function getType(el) { var dataType = $(el).data('data-type'); if (dataType === 'date') return 'shiny.date';else if (dataType === 'datetime') return 'shiny.datetime';else return false; }, getValue: function getValue(el) { var $el = $(el); var result = $(el).data('ionRangeSlider').result; // Function for converting numeric value from slider to appropriate type. var convert; var dataType = $el.data('data-type'); if (dataType === 'date') { convert = function convert(val) { return formatDateUTC(new Date(+val)); }; } else if (dataType === 'datetime') { convert = function convert(val) { // Convert ms to s return +val / 1000; }; } else { convert = function convert(val) { return +val; }; } if (this._numValues(el) === 2) { return [convert(result.from), convert(result.to)]; } else { return convert(result.from); } }, setValue: function setValue(el, value) { var $el = $(el); var slider = $el.data('ionRangeSlider'); $el.data('immediate', true); try { if (this._numValues(el) === 2 && value instanceof Array) { slider.update({ from: value[0], to: value[1] }); } else { slider.update({ from: value }); } forceIonSliderUpdate(slider); } finally { $el.data('immediate', false); } }, subscribe: function subscribe(el, callback) { $(el).on('change.sliderInputBinding', function (event) { callback(!$(el).data('immediate') && !$(el).data('animating')); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.sliderInputBinding'); }, receiveMessage: function receiveMessage(el, data) { var $el = $(el); var slider = $el.data('ionRangeSlider'); var msg = {}; if (data.hasOwnProperty('value')) { if (this._numValues(el) === 2 && data.value instanceof Array) { msg.from = data.value[0]; msg.to = data.value[1]; } else { msg.from = data.value; } } var sliderFeatures = ['min', 'max', 'step']; for (var i = 0; i < sliderFeatures.length; i++) { var feats = sliderFeatures[i]; if (data.hasOwnProperty(feats)) { msg[feats] = data[feats]; } } updateLabel(data.label, this._getLabelNode(el)); var domElements = ['data-type', 'time-format', 'timezone']; for (var i = 0; i < domElements.length; i++) { var elem = domElements[i]; if (data.hasOwnProperty(elem)) { $el.data(elem, data[elem]); } } var dataType = $el.data('data-type'); var timeFormat = $el.data('time-format'); var timezone = $el.data('timezone'); msg.prettify = getTypePrettifyer(dataType, timeFormat, timezone); $el.data('immediate', true); try { slider.update(msg); forceIonSliderUpdate(slider); } finally { $el.data('immediate', false); } }, getRatePolicy: function getRatePolicy() { return { policy: 'debounce', delay: 250 }; }, getState: function getState(el) {}, initialize: function initialize(el) { var opts = {}; var $el = $(el); var dataType = $el.data('data-type'); var timeFormat = $el.data('time-format'); var timezone = $el.data('timezone'); opts.prettify = getTypePrettifyer(dataType, timeFormat, timezone); $el.ionRangeSlider(opts); }, _getLabelNode: function _getLabelNode(el) { return $(el).parent().find('label[for="' + $escape(el.id) + '"]'); }, // Number of values; 1 for single slider, 2 for range slider _numValues: function _numValues(el) { if ($(el).data('ionRangeSlider').options.type === 'double') return 2;else return 1; } }); inputBindings.register(sliderInputBinding, 'shiny.sliderInput'); // Format numbers for nicer output. // formatNumber(1234567.12345) === "1,234,567.12345" // formatNumber(1234567.12345, ".", ",") === "1.234.567,12345" // formatNumber(1000, " ") === "1 000" // formatNumber(20) === "20" // formatNumber(1.2345e24) === "1.2345e+24" function formatNumber(num) { var thousand_sep = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ","; var decimal_sep = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "."; var parts = num.toString().split("."); // Add separators to portion before decimal mark. parts[0] = parts[0].replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g, "$1" + thousand_sep); if (parts.length === 1) return parts[0];else if (parts.length === 2) return parts[0] + decimal_sep + parts[1];else return ""; } ; $(document).on('click', '.slider-animate-button', function (evt) { evt.preventDefault(); var self = $(this); var target = $('#' + $escape(self.attr('data-target-id'))); var startLabel = 'Play'; var stopLabel = 'Pause'; var loop = self.attr('data-loop') !== undefined && !/^\s*false\s*$/i.test(self.attr('data-loop')); var animInterval = self.attr('data-interval'); if (isNaN(animInterval)) animInterval = 1500;else animInterval = +animInterval; if (!target.data('animTimer')) { var slider; var timer; // Separate code paths: // Backward compatible code for old-style jsliders (Shiny <= 0.10.2.2), // and new-style ionsliders. if (target.hasClass('jslider')) { slider = target.slider(); // If we're currently at the end, restart if (!slider.canStepNext()) slider.resetToStart(); timer = setInterval(function () { if (loop && !slider.canStepNext()) { slider.resetToStart(); } else { slider.stepNext(); if (!loop && !slider.canStepNext()) { self.click(); // stop the animation } } }, animInterval); } else { slider = target.data('ionRangeSlider'); // Single sliders have slider.options.type == "single", and only the // `from` value is used. Double sliders have type == "double", and also // use the `to` value for the right handle. var sliderCanStep = function sliderCanStep() { if (slider.options.type === "double") return slider.result.to < slider.result.max;else return slider.result.from < slider.result.max; }; var sliderReset = function sliderReset() { var val = { from: slider.result.min }; // Preserve the current spacing for double sliders if (slider.options.type === "double") val.to = val.from + (slider.result.to - slider.result.from); slider.update(val); forceIonSliderUpdate(slider); }; var sliderStep = function sliderStep() { // Don't overshoot the end var val = { from: Math.min(slider.result.max, slider.result.from + slider.options.step) }; if (slider.options.type === "double") val.to = Math.min(slider.result.max, slider.result.to + slider.options.step); slider.update(val); forceIonSliderUpdate(slider); }; // If we're currently at the end, restart if (!sliderCanStep()) sliderReset(); timer = setInterval(function () { if (loop && !sliderCanStep()) { sliderReset(); } else { sliderStep(); if (!loop && !sliderCanStep()) { self.click(); // stop the animation } } }, animInterval); } target.data('animTimer', timer); self.attr('title', stopLabel); self.addClass('playing'); target.data('animating', true); } else { clearTimeout(target.data('animTimer')); target.removeData('animTimer'); self.attr('title', startLabel); self.removeClass('playing'); target.removeData('animating'); } }); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_date.js var dateInputBinding = new InputBinding(); $.extend(dateInputBinding, { find: function find(scope) { return $(scope).find('.shiny-date-input'); }, getType: function getType(el) { return "shiny.date"; }, // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a // format like mm/dd/yyyy) getValue: function getValue(el) { var date = $(el).find('input').bsDatepicker('getUTCDate'); return formatDateUTC(date); }, // value must be an unambiguous string like '2001-01-01', or a Date object. setValue: function setValue(el, value) { // R's NA, which is null here will remove current value if (value === null) { $(el).find('input').val('').bsDatepicker('update'); return; } var date = this._newDate(value); // If date is invalid, do nothing if (isNaN(date)) return; $(el).find('input').bsDatepicker('setUTCDate', date); }, getState: function getState(el) { var $el = $(el); var $input = $el.find('input'); var min = $input.data('datepicker').startDate; var max = $input.data('datepicker').endDate; // Stringify min and max. If min and max aren't set, they will be // -Infinity and Infinity; replace these with null. min = min === -Infinity ? null : formatDateUTC(min); max = max === Infinity ? null : formatDateUTC(max); // startViewMode is stored as a number; convert to string var startview = $input.data('datepicker').startViewMode; if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month'; return { label: this._getLabelNode(el).text(), value: this.getValue(el), valueString: $input.val(), min: min, max: max, language: $input.data('datepicker').language, weekstart: $input.data('datepicker').weekStart, format: this._formatToString($input.data('datepicker').format), startview: startview }; }, receiveMessage: function receiveMessage(el, data) { var $input = $(el).find('input'); updateLabel(data.label, this._getLabelNode(el)); if (data.hasOwnProperty('min')) this._setMin($input[0], data.min); if (data.hasOwnProperty('max')) this._setMax($input[0], data.max); // Must set value only after min and max have been set. If new value is // outside the bounds of the previous min/max, then the result will be a // blank input. if (data.hasOwnProperty('value')) this.setValue(el, data.value); $(el).trigger('change'); }, subscribe: function subscribe(el, callback) { $(el).on('keyup.dateInputBinding input.dateInputBinding', function (event) { // Use normal debouncing policy when typing callback(true); }); $(el).on('changeDate.dateInputBinding change.dateInputBinding', function (event) { // Send immediately when clicked callback(false); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.dateInputBinding'); }, getRatePolicy: function getRatePolicy() { return { policy: 'debounce', delay: 250 }; }, initialize: function initialize(el) { var $input = $(el).find('input'); var date = $input.data('initial-date'); // If initial_date is null, set to current date if (date === undefined || date === null) { // Get local date, but as UTC date = this._dateAsUTC(new Date()); } this.setValue(el, date); // Set the start and end dates, from min-date and max-date. These always // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in // support for date-startdate and data-enddate, which use the current // date format. if ($input.data('min-date') !== undefined) { this._setMin($input[0], $input.data('min-date')); } if ($input.data('max-date') !== undefined) { this._setMax($input[0], $input.data('max-date')); } }, _getLabelNode: function _getLabelNode(el) { return $(el).find('label[for="' + $escape(el.id) + '"]'); }, // Given a format object from a date picker, return a string _formatToString: function _formatToString(format) { // Format object has structure like: // { parts: ['mm', 'dd', 'yy'], separators: ['', '/', '/' ,''] } var str = ''; for (var i = 0; i < format.parts.length; i++) { str += format.separators[i] + format.parts[i]; } str += format.separators[i]; return str; }, // Given an unambiguous date string or a Date object, set the min (start) date. // null will unset. undefined will result in no change, _setMin: function _setMin(el, date) { if (date === undefined) return; if (date === null) { $(el).bsDatepicker('setStartDate', null); return; } date = this._newDate(date); // If date parsing fails, do nothing if (date === null) return; date = this._UTCDateAsLocal(date); if (isNaN(date)) return; // Workaround for https://github.com/eternicode/bootstrap-datepicker/issues/2010 // If the start date when there's a two-digit year format, it will set // the date value to null. So we'll save the value, set the start // date, and the restore the value. var curValue = $(el).bsDatepicker('getUTCDate'); $(el).bsDatepicker('setStartDate', date); $(el).bsDatepicker('setUTCDate', curValue); // Workaround for https://github.com/rstudio/shiny/issues/2335 // We only set the start date *after* the value in this special // case so we don't effect the intended behavior of having a blank // value when it falls outside the start date if (typeof date.toDateString !== 'function') return; if (typeof curValue.toDateString !== 'function') return; if (date.toDateString() === curValue.toDateString()) { $(el).bsDatepicker('setStartDate', null); $(el).bsDatepicker('setUTCDate', curValue); $(el).bsDatepicker('setStartDate', date); } }, // Given an unambiguous date string or a Date object, set the max (end) date // null will unset. _setMax: function _setMax(el, date) { if (date === undefined) return; if (date === null) { $(el).bsDatepicker('setEndDate', null); return; } date = this._newDate(date); // If date parsing fails, do nothing if (date === null) return; date = this._UTCDateAsLocal(date); if (isNaN(date)) return; // Workaround for same issue as in _setMin. var curValue = $(el).bsDatepicker('getUTCDate'); $(el).bsDatepicker('setEndDate', date); $(el).bsDatepicker('setUTCDate', curValue); // Workaround for same issue as in _setMin. if (typeof date.toDateString !== 'function') return; if (typeof curValue.toDateString !== 'function') return; if (date.toDateString() === curValue.toDateString()) { $(el).bsDatepicker('setEndDate', null); $(el).bsDatepicker('setUTCDate', curValue); $(el).bsDatepicker('setEndDate', date); } }, // Given a date string of format yyyy-mm-dd, return a Date object with // that date at 12AM UTC. // If date is a Date object, return it unchanged. _newDate: function _newDate(date) { if (date instanceof Date) return date; if (!date) return null; // Get Date object - this will be at 12AM in UTC, but may print // differently at the Javascript console. var d = parseDate(date); // If invalid date, return null if (isNaN(d)) return null; return new Date(d.getTime()); }, // Given a Date object, return a Date object which has the same "clock time" // in UTC. For example, if input date is 2013-02-01 23:00:00 GMT-0600 (CST), // output will be 2013-02-01 23:00:00 UTC. Note that the JS console may // print this in local time, as "Sat Feb 02 2013 05:00:00 GMT-0600 (CST)". _dateAsUTC: function _dateAsUTC(date) { return new Date(date.getTime() - date.getTimezoneOffset() * 60000); }, // The inverse of _dateAsUTC. This is needed to adjust time zones because // some bootstrap-datepicker methods only take local dates as input, and not // UTC. _UTCDateAsLocal: function _UTCDateAsLocal(date) { return new Date(date.getTime() + date.getTimezoneOffset() * 60000); } }); inputBindings.register(dateInputBinding, 'shiny.dateInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_daterange.js var dateRangeInputBinding = {}; $.extend(dateRangeInputBinding, dateInputBinding, { find: function find(scope) { return $(scope).find('.shiny-date-range-input'); }, // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a // format like mm/dd/yyyy) getValue: function getValue(el) { var $inputs = $(el).find('input'); var start = $inputs.eq(0).bsDatepicker('getUTCDate'); var end = $inputs.eq(1).bsDatepicker('getUTCDate'); return [formatDateUTC(start), formatDateUTC(end)]; }, // value must be an object, with optional fields `start` and `end`. These // should be unambiguous strings like '2001-01-01', or Date objects. setValue: function setValue(el, value) { if (!(value instanceof Object)) { return; } // Get the start and end input objects var $inputs = $(el).find('input'); // If value is undefined, don't try to set // null will remove the current value if (value.start !== undefined) { if (value.start === null) { $inputs.eq(0).val('').bsDatepicker('update'); } else { var start = this._newDate(value.start); $inputs.eq(0).bsDatepicker('setUTCDate', start); } } if (value.end !== undefined) { if (value.end === null) { $inputs.eq(1).val('').bsDatepicker('update'); } else { var end = this._newDate(value.end); $inputs.eq(1).bsDatepicker('setUTCDate', end); } } }, getState: function getState(el) { var $el = $(el); var $inputs = $el.find('input'); var $startinput = $inputs.eq(0); var $endinput = $inputs.eq(1); // For many of the properties, assume start and end have the same values var min = $startinput.bsDatepicker('getStartDate'); var max = $startinput.bsDatepicker('getEndDate'); // Stringify min and max. If min and max aren't set, they will be // -Infinity and Infinity; replace these with null. min = min === -Infinity ? null : formatDateUTC(min); max = max === Infinity ? null : formatDateUTC(max); // startViewMode is stored as a number; convert to string var startview = $startinput.data('datepicker').startView; if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month'; return { label: this._getLabelNode(el).text(), value: this.getValue(el), valueString: [$startinput.val(), $endinput.val()], min: min, max: max, weekstart: $startinput.data('datepicker').weekStart, format: this._formatToString($startinput.data('datepicker').format), language: $startinput.data('datepicker').language, startview: startview }; }, receiveMessage: function receiveMessage(el, data) { var $el = $(el); var $inputs = $el.find('input'); var $startinput = $inputs.eq(0); var $endinput = $inputs.eq(1); updateLabel(data.label, this._getLabelNode(el)); if (data.hasOwnProperty('min')) { this._setMin($startinput[0], data.min); this._setMin($endinput[0], data.min); } if (data.hasOwnProperty('max')) { this._setMax($startinput[0], data.max); this._setMax($endinput[0], data.max); } // Must set value only after min and max have been set. If new value is // outside the bounds of the previous min/max, then the result will be a // blank input. if (data.hasOwnProperty('value')) this.setValue(el, data.value); $el.trigger('change'); }, initialize: function initialize(el) { var $el = $(el); var $inputs = $el.find('input'); var $startinput = $inputs.eq(0); var $endinput = $inputs.eq(1); var start = $startinput.data('initial-date'); var end = $endinput.data('initial-date'); // If empty/null, use local date, but as UTC if (start === undefined || start === null) start = this._dateAsUTC(new Date()); if (end === undefined || end === null) end = this._dateAsUTC(new Date()); this.setValue(el, { "start": start, "end": end }); // // Set the start and end dates, from min-date and max-date. These always // // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in // // support for date-startdate and data-enddate, which use the current // // date format. this._setMin($startinput[0], $startinput.data('min-date')); this._setMin($endinput[0], $startinput.data('min-date')); this._setMax($startinput[0], $endinput.data('max-date')); this._setMax($endinput[0], $endinput.data('max-date')); }, subscribe: function subscribe(el, callback) { $(el).on('keyup.dateRangeInputBinding input.dateRangeInputBinding', function (event) { // Use normal debouncing policy when typing callback(true); }); $(el).on('changeDate.dateRangeInputBinding change.dateRangeInputBinding', function (event) { // Send immediately when clicked callback(false); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.dateRangeInputBinding'); }, _getLabelNode: function _getLabelNode(el) { return $(el).find('label[for="' + $escape(el.id) + '"]'); } }); inputBindings.register(dateRangeInputBinding, 'shiny.dateRangeInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_select.js var selectInputBinding = new InputBinding(); $.extend(selectInputBinding, { find: function find(scope) { return $(scope).find('select'); }, getType: function getType(el) { var $el = $(el); if (!$el.hasClass("symbol")) { // default character type return null; } if ($el.attr("multiple") === "multiple") { return 'shiny.symbolList'; } else { return 'shiny.symbol'; } }, getId: function getId(el) { return InputBinding.prototype.getId.call(this, el) || el.name; }, getValue: function getValue(el) { return $(el).val(); }, setValue: function setValue(el, value) { if (!this._is_selectize(el)) { $(el).val(value); } else { var selectize = this._selectize(el); if (selectize) { selectize.setValue(value); } } }, getState: function getState(el) { // Store options in an array of objects, each with with value and label var options = new Array(el.length); for (var i = 0; i < el.length; i++) { options[i] = { value: el[i].value, label: el[i].label }; } return { label: this._getLabelNode(el), value: this.getValue(el), options: options }; }, receiveMessage: function receiveMessage(el, data) { var $el = $(el), selectize; // This will replace all the options if (data.hasOwnProperty('options')) { selectize = this._selectize(el); // Must destroy selectize before appending new options, otherwise // selectize will restore the original select if (selectize) selectize.destroy(); // Clear existing options and add each new one $el.empty().append(data.options); this._selectize(el); } // re-initialize selectize if (data.hasOwnProperty('config')) { $el.parent().find('script[data-for="' + $escape(el.id) + '"]').replaceWith(data.config); this._selectize(el, true); } // use server-side processing for selectize if (data.hasOwnProperty('url')) { selectize = this._selectize(el); selectize.clearOptions(); var loaded = false; selectize.settings.load = function (query, callback) { var settings = selectize.settings; $.ajax({ url: data.url, data: { query: query, field: JSON.stringify([settings.searchField]), value: settings.valueField, conju: settings.searchConjunction, maxop: settings.maxOptions }, type: 'GET', error: function error() { callback(); }, success: function success(res) { // res = [{label: '1', value: '1', group: '1'}, ...] // success is called after options are added, but // groups need to be added manually below $.each(res, function (index, elem) { // Call selectize.addOptionGroup once for each optgroup; the // first argument is the group ID, the second is an object with // the group's label and value. We use the current settings of // the selectize object to decide the fieldnames of that obj. var optgroupId = elem[settings.optgroupField || "optgroup"]; var optgroup = {}; optgroup[settings.optgroupLabelField || "label"] = optgroupId; optgroup[settings.optgroupValueField || "value"] = optgroupId; selectize.addOptionGroup(optgroupId, optgroup); }); callback(res); if (!loaded) { if (data.hasOwnProperty('value')) { selectize.setValue(data.value); } else if (settings.maxItems === 1) { // first item selected by default only for single-select selectize.setValue(res[0].value); } } loaded = true; } }); }; // perform an empty search after changing the `load` function selectize.load(function (callback) { selectize.settings.load.apply(selectize, ['', callback]); }); } else if (data.hasOwnProperty('value')) { this.setValue(el, data.value); } updateLabel(data.label, this._getLabelNode(el)); $(el).trigger('change'); }, subscribe: function subscribe(el, callback) { var _this = this; $(el).on('change.selectInputBinding', function (event) { // https://github.com/rstudio/shiny/issues/2162 // Prevent spurious events that are gonna be squelched in // a second anyway by the onItemRemove down below if (el.nonempty && _this.getValue(el) === "") { return; } callback(); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.selectInputBinding'); }, initialize: function initialize(el) { this._selectize(el); }, _getLabelNode: function _getLabelNode(el) { var escaped_id = $escape(el.id); if (this._is_selectize(el)) { escaped_id += "-selectized"; } return $(el).parent().parent().find('label[for="' + escaped_id + '"]'); }, // Return true if it's a selectize input, false if it's a regular select input. _is_selectize: function _is_selectize(el) { var config = $(el).parent().find('script[data-for="' + $escape(el.id) + '"]'); return config.length > 0; }, _selectize: function _selectize(el, update) { if (!$.fn.selectize) return undefined; var $el = $(el); var config = $el.parent().find('script[data-for="' + $escape(el.id) + '"]'); if (config.length === 0) return undefined; var options = $.extend({ labelField: 'label', valueField: 'value', searchField: ['label'] }, JSON.parse(config.html())); // selectize created from selectInput() if (typeof config.data('nonempty') !== 'undefined') { el.nonempty = true; options = $.extend(options, { onItemRemove: function onItemRemove(value) { if (this.getValue() === "") $("select#" + $escape(el.id)).empty().append($("<option/>", { "value": value, "selected": true })).trigger("change"); }, onDropdownClose: function onDropdownClose($dropdown) { if (this.getValue() === "") this.setValue($("select#" + $escape(el.id)).val()); } }); } else { el.nonempty = false; } // options that should be eval()ed if (config.data('eval') instanceof Array) $.each(config.data('eval'), function (i, x) { /*jshint evil: true*/ options[x] = eval('(' + options[x] + ')'); }); var control = $el.selectize(options)[0].selectize; // .selectize() does not really update settings; must destroy and rebuild if (update) { var settings = $.extend(control.settings, options); control.destroy(); control = $el.selectize(settings)[0].selectize; } return control; } }); inputBindings.register(selectInputBinding, 'shiny.selectInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_radio.js var radioInputBinding = new InputBinding(); $.extend(radioInputBinding, { find: function find(scope) { return $(scope).find('.shiny-input-radiogroup'); }, getValue: function getValue(el) { // Select the radio objects that have name equal to the grouping div's id return $('input:radio[name="' + $escape(el.id) + '"]:checked').val(); }, setValue: function setValue(el, value) { $('input:radio[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true); }, getState: function getState(el) { var $objs = $('input:radio[name="' + $escape(el.id) + '"]'); // Store options in an array of objects, each with with value and label var options = new Array($objs.length); for (var i = 0; i < options.length; i++) { options[i] = { value: $objs[i].value, label: this._getLabel($objs[i]) }; } return { label: this._getLabelNode(el).text(), value: this.getValue(el), options: options }; }, receiveMessage: function receiveMessage(el, data) { var $el = $(el); // This will replace all the options if (data.hasOwnProperty('options')) { // Clear existing options and add each new one $el.find('div.shiny-options-group').remove(); // Backward compatibility: for HTML generated by shinybootstrap2 package $el.find('label.radio').remove(); $el.append(data.options); } if (data.hasOwnProperty('value')) this.setValue(el, data.value); updateLabel(data.label, this._getLabelNode(el)); $(el).trigger('change'); }, subscribe: function subscribe(el, callback) { $(el).on('change.radioInputBinding', function (event) { callback(); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.radioInputBinding'); }, // Get the DOM element that contains the top-level label _getLabelNode: function _getLabelNode(el) { return $(el).parent().find('label[for="' + $escape(el.id) + '"]'); }, // Given an input DOM object, get the associated label. Handles labels // that wrap the input as well as labels associated with 'for' attribute. _getLabel: function _getLabel(obj) { // If <label><input /><span>label text</span></label> if (obj.parentNode.tagName === "LABEL") { return $(obj.parentNode).find('span').text().trim(); } return null; }, // Given an input DOM object, set the associated label. Handles labels // that wrap the input as well as labels associated with 'for' attribute. _setLabel: function _setLabel(obj, value) { // If <label><input /><span>label text</span></label> if (obj.parentNode.tagName === "LABEL") { $(obj.parentNode).find('span').text(value); } return null; } }); inputBindings.register(radioInputBinding, 'shiny.radioInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_checkboxgroup.js var checkboxGroupInputBinding = new InputBinding(); $.extend(checkboxGroupInputBinding, { find: function find(scope) { return $(scope).find('.shiny-input-checkboxgroup'); }, getValue: function getValue(el) { // Select the checkbox objects that have name equal to the grouping div's id var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]:checked'); var values = new Array($objs.length); for (var i = 0; i < $objs.length; i++) { values[i] = $objs[i].value; } return values; }, setValue: function setValue(el, value) { // Clear all checkboxes $('input:checkbox[name="' + $escape(el.id) + '"]').prop('checked', false); // Accept array if (value instanceof Array) { for (var i = 0; i < value.length; i++) { $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value[i]) + '"]').prop('checked', true); } // Else assume it's a single value } else { $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true); } }, getState: function getState(el) { var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]'); // Store options in an array of objects, each with with value and label var options = new Array($objs.length); for (var i = 0; i < options.length; i++) { options[i] = { value: $objs[i].value, label: this._getLabel($objs[i]) }; } return { label: this._getLabelNode(el).text(), value: this.getValue(el), options: options }; }, receiveMessage: function receiveMessage(el, data) { var $el = $(el); // This will replace all the options if (data.hasOwnProperty('options')) { // Clear existing options and add each new one $el.find('div.shiny-options-group').remove(); // Backward compatibility: for HTML generated by shinybootstrap2 package $el.find('label.checkbox').remove(); $el.append(data.options); } if (data.hasOwnProperty('value')) this.setValue(el, data.value); updateLabel(data.label, this._getLabelNode(el)); $(el).trigger('change'); }, subscribe: function subscribe(el, callback) { $(el).on('change.checkboxGroupInputBinding', function (event) { callback(); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.checkboxGroupInputBinding'); }, // Get the DOM element that contains the top-level label _getLabelNode: function _getLabelNode(el) { return $(el).find('label[for="' + $escape(el.id) + '"]'); }, // Given an input DOM object, get the associated label. Handles labels // that wrap the input as well as labels associated with 'for' attribute. _getLabel: function _getLabel(obj) { // If <label><input /><span>label text</span></label> if (obj.parentNode.tagName === "LABEL") { return $(obj.parentNode).find('span').text().trim(); } return null; }, // Given an input DOM object, set the associated label. Handles labels // that wrap the input as well as labels associated with 'for' attribute. _setLabel: function _setLabel(obj, value) { // If <label><input /><span>label text</span></label> if (obj.parentNode.tagName === "LABEL") { $(obj.parentNode).find('span').text(value); } return null; } }); inputBindings.register(checkboxGroupInputBinding, 'shiny.checkboxGroupInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_actionbutton.js var actionButtonInputBinding = new InputBinding(); $.extend(actionButtonInputBinding, { find: function find(scope) { return $(scope).find(".action-button"); }, getValue: function getValue(el) { return $(el).data('val') || 0; }, setValue: function setValue(el, value) { $(el).data('val', value); }, getType: function getType(el) { return 'shiny.action'; }, subscribe: function subscribe(el, callback) { $(el).on("click.actionButtonInputBinding", function (e) { var $el = $(this); var val = $el.data('val') || 0; $el.data('val', val + 1); callback(); }); }, getState: function getState(el) { return { value: this.getValue(el) }; }, receiveMessage: function receiveMessage(el, data) { var $el = $(el); // retrieve current label and icon var label = $el.text(); var icon = ''; // to check (and store) the previous icon, we look for a $el child // object that has an i tag, and some (any) class (this prevents // italicized text - which has an i tag but, usually, no class - // from being mistakenly selected) if ($el.find('i[class]').length > 0) { var icon_html = $el.find('i[class]')[0]; if (icon_html === $el.children()[0]) { // another check for robustness icon = $(icon_html).prop('outerHTML'); } } // update the requested properties if (data.hasOwnProperty('label')) label = data.label; if (data.hasOwnProperty('icon')) { icon = data.icon; // if the user entered icon=character(0), remove the icon if (icon.length === 0) icon = ''; } // produce new html $el.html(icon + ' ' + label); }, unsubscribe: function unsubscribe(el) { $(el).off(".actionButtonInputBinding"); } }); inputBindings.register(actionButtonInputBinding, 'shiny.actionButtonInput'); $(document).on('click', 'a.action-button', function (e) { e.preventDefault(); }); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_tabinput.js var bootstrapTabInputBinding = new InputBinding(); $.extend(bootstrapTabInputBinding, { find: function find(scope) { return $(scope).find('ul.nav.shiny-tab-input'); }, getValue: function getValue(el) { var anchor = $(el).find('li:not(.dropdown).active').children('a'); if (anchor.length === 1) return this._getTabName(anchor); return null; }, setValue: function setValue(el, value) { var self = this; var success = false; if (value) { var anchors = $(el).find('li:not(.dropdown)').children('a'); anchors.each(function () { if (self._getTabName($(this)) === value) { $(this).tab('show'); success = true; return false; // Break out of each() } return true; }); } if (!success) { // This is to handle the case where nothing is selected, e.g. the last tab // was removed using removeTab. $(el).trigger("change"); } }, getState: function getState(el) { return { value: this.getValue(el) }; }, receiveMessage: function receiveMessage(el, data) { if (data.hasOwnProperty('value')) this.setValue(el, data.value); }, subscribe: function subscribe(el, callback) { $(el).on('change shown.bootstrapTabInputBinding shown.bs.tab.bootstrapTabInputBinding', function (event) { callback(); }); }, unsubscribe: function unsubscribe(el) { $(el).off('.bootstrapTabInputBinding'); }, _getTabName: function _getTabName(anchor) { return anchor.attr('data-value') || anchor.text(); } }); inputBindings.register(bootstrapTabInputBinding, 'shiny.bootstrapTabInput'); //--------------------------------------------------------------------- // Source file: ../srcjs/input_binding_fileinput.js var IE8FileUploader = function IE8FileUploader(shinyapp, id, fileEl) { this.shinyapp = shinyapp; this.id = id; this.fileEl = fileEl; this.beginUpload(); }; (function () { this.beginUpload = function () { var self = this; // Create invisible frame var iframeId = 'shinyupload_iframe_' + this.id; this.iframe = document.createElement('iframe'); this.iframe.id = iframeId; this.iframe.name = iframeId; this.iframe.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0; border: none'); $(document.body).append(this.iframe); var iframeDestroy = function iframeDestroy() { // Forces Shiny to flushReact, flush outputs, etc. Without this we get // invalidated reactives, but observers don't actually execute. self.shinyapp.makeRequest('uploadieFinish', [], function () {}, function () {}); $(self.iframe).remove(); // Reset the file input's value to "". This allows the same file to be // uploaded again. https://stackoverflow.com/a/22521275 $(self.fileEl).val(""); }; if (this.iframe.attachEvent) { this.iframe.attachEvent('onload', iframeDestroy); } else { this.iframe.onload = iframeDestroy; } this.form = document.createElement('form'); this.form.method = 'POST'; this.form.setAttribute('enctype', 'multipart/form-data'); this.form.action = "session/" + encodeURI(this.shinyapp.config.sessionId) + "/uploadie/" + encodeURI(this.id); this.form.id = 'shinyupload_form_' + this.id; this.form.target = iframeId; $(this.form).insertAfter(this.fileEl).append(this.fileEl); this.form.submit(); }; }).call(IE8FileUploader.prototype); var FileUploader = function FileUploader(shinyapp, id, files, el) { this.shinyapp = shinyapp; this.id = id; this.el = el; FileProcessor.call(this, files); }; $.extend(FileUploader.prototype, FileProcessor.prototype); (function () { this.makeRequest = function (method, args, onSuccess, onFailure, blobs) { this.shinyapp.makeRequest(method, args, onSuccess, onFailure, blobs); }; this.onBegin = function (files, cont) { var self = this; // Reset progress bar this.$setError(null); this.$setActive(true); this.$setVisible(true); this.onProgress(null, 0); this.totalBytes = 0; this.progressBytes = 0; $.each(files, function (i, file) { self.totalBytes += file.size; }); var fileInfo = $.map(files, function (file, i) { return { name: file.name, size: file.size, type: file.type }; }); this.makeRequest('uploadInit', [fileInfo], function (response) { self.jobId = response.jobId; self.uploadUrl = response.uploadUrl; cont(); }, function (error) { self.onError(error); }); }; this.onFile = function (file, cont) { var self = this; this.onProgress(file, 0); $.ajax(this.uploadUrl, { type: 'POST', cache: false, xhr: function xhr() { var xhrVal = $.ajaxSettings.xhr(); if (xhrVal.upload) { xhrVal.upload.onprogress = function (e) { if (e.lengthComputable) { self.onProgress(file, (self.progressBytes + e.loaded) / self.totalBytes); } }; } return xhrVal; }, data: file, contentType: 'application/octet-stream', processData: false, success: function success() { self.progressBytes += file.size; cont(); }, error: function error(jqXHR, textStatus, errorThrown) { self.onError(jqXHR.responseText || textStatus); } }); }; this.onComplete = function () { var self = this; var fileInfo = $.map(this.files, function (file, i) { return { name: file.name, size: file.size, type: file.type }; }); // Trigger shiny:inputchanged. Unlike a normal shiny:inputchanged event, // it's not possible to modify the information before the values get // sent to the server. var evt = jQuery.Event("shiny:inputchanged"); evt.name = this.id; evt.value = fileInfo; evt.binding = fileInputBinding; evt.el = this.el; evt.inputType = 'shiny.fileupload'; $(document).trigger(evt); this.makeRequest('uploadEnd', [this.jobId, this.id], function (response) { self.$setActive(false); self.onProgress(null, 1); self.$bar().text('Upload complete'); // Reset the file input's value to "". This allows the same file to be // uploaded again. https://stackoverflow.com/a/22521275 $(evt.el).val(""); }, function (error) { self.onError(error); }); this.$bar().text('Finishing upload'); }; this.onError = function (message) { this.$setError(message || ''); this.$setActive(false); }; this.onAbort = function () { this.$setVisible(false); }; this.onProgress = function (file, completed) { this.$bar().width(Math.round(completed * 100) + '%'); this.$bar().text(file ? file.name : ''); }; this.$container = function () { return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress'); }; this.$bar = function () { return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress .progress-bar'); }; this.$setVisible = function (visible) { this.$container().css('visibility', visible ? 'visible' : 'hidden'); }; this.$setError = function (error) { this.$bar().toggleClass('progress-bar-danger', error !== null); if (error !== null) { this.onProgress(null, 1); this.$bar().text(error); } }; this.$setActive = function (active) { this.$container().toggleClass('active', !!active); }; }).call(FileUploader.prototype); // NOTE On Safari, at least version 10.1.2, *if the developer console is open*, // setting the input's value will behave strangely because of a Safari bug. The // uploaded file's name will appear over the placeholder value, instead of // replacing it. The workaround is to restart Safari. When I (Alan Dipert) ran // into this bug Winston Chang helped me diagnose the exact problem, and Winston // then submitted a bug report to Apple. function setFileText($el, files) { var $fileText = $el.closest('div.input-group').find('input[type=text]'); if (files.length === 1) { $fileText.val(files[0].name); } else { $fileText.val(files.length + " files"); } } // If previously selected files are uploading, abort that. function abortCurrentUpload($el) { var uploader = $el.data('currentUploader'); if (uploader) uploader.abort(); // Clear data-restore attribute if present. $el.removeAttr('data-restore'); } function uploadDroppedFilesIE10Plus(el, files) { var $el = $(el); abortCurrentUpload($el); // Set the label in the text box setFileText($el, files); // Start the new upload and put the uploader in 'currentUploader'. $el.data('currentUploader', new FileUploader(exports.shinyapp, fileInputBinding.getId(el), files, el)); } function uploadFiles(evt) { var $el = $(evt.target); abortCurrentUpload($el); var files = evt.target.files; // IE8 here does not necessarily mean literally IE8; it indicates if the web // browser supports the FileList object (IE8/9 do not support it) var IE8 = typeof files === 'undefined'; var id = fileInputBinding.getId(evt.target); if (!IE8 && files.length === 0) return; // Set the label in the text box var $fileText = $el.closest('div.input-group').find('input[type=text]'); if (IE8) { // If we're using IE8/9, just use this placeholder $fileText.val("[Uploaded file]"); } else { setFileText($el, files); } // Start the new upload and put the uploader in 'currentUploader'. if (IE8) { /*jshint nonew:false */ new IE8FileUploader(exports.shinyapp, id, evt.target); } else { $el.data('currentUploader', new FileUploader(exports.shinyapp, id, files, evt.target)); } } // Here we maintain a list of all the current file inputs. This is necessary // because we need to trigger events on them in order to respond to file drag // events. For example, they should all light up when a file is dragged on to // the page. var $fileInputs = $(); var fileInputBinding = new InputBinding(); $.extend(fileInputBinding, { find: function find(scope) { return $(scope).find('input[type="file"]'); }, getId: function getId(el) { return InputBinding.prototype.getId.call(this, el) || el.name; }, getValue: function getValue(el) { // This returns a non-undefined value only when there's a 'data-restore' // attribute, which is set only when restoring Shiny state. If a file is // uploaded through the browser, 'data-restore' gets cleared. var data = $(el).attr('data-restore'); if (data) { data = JSON.parse(data); // Set the label in the text box var $fileText = $(el).closest('div.input-group').find('input[type=text]'); if (data.name.length === 1) { $fileText.val(data.name[0]); } else { $fileText.val(data.name.length + " files"); } // Manually set up progress bar. A bit inelegant because it duplicates // code from FileUploader, but duplication is less bad than alternatives. var $progress = $(el).closest('div.form-group').find('.progress'); var $bar = $progress.find('.progress-bar'); $progress.removeClass('active'); $bar.width('100%'); $bar.css('visibility', 'visible'); return data; } else { return null; } }, setValue: function setValue(el, value) {// Not implemented }, getType: function getType(el) { // This will be used only when restoring a file from a saved state. return 'shiny.file'; }, _zoneOf: function _zoneOf(el) { return $(el).closest("div.input-group"); }, // This function makes it possible to attach listeners to the dragenter, // dragleave, and drop events of a single element with children. It's not // intuitive to do directly because outer elements fire "dragleave" events // both when the drag leaves the element and when the drag enters a child. To // make it easier, we maintain a count of the elements being dragged across // and trigger 3 new types of event: // // 1. draghover:enter - When a drag enters el and any of its children. // 2. draghover:leave - When the drag leaves el and all of its children. // 3. draghover:drop - When an item is dropped on el or any of its children. _enableDraghover: function _enableDraghover(el) { var $el = $(el), childCounter = 0; $el.on({ "dragenter.draghover": function dragenterDraghover(e) { if (childCounter++ === 0) { $el.trigger("draghover:enter", e); } }, "dragleave.draghover": function dragleaveDraghover(e) { if (--childCounter === 0) { $el.trigger("draghover:leave", e); } if (childCounter < 0) { console.error("draghover childCounter is negative somehow"); } }, "dragover.draghover": function dragoverDraghover(e) { e.preventDefault(); }, "drop.draghover": function dropDraghover(e) { childCounter = 0; $el.trigger("draghover:drop", e); e.preventDefault(); } }); return $el; }, _disableDraghover: function _disableDraghover(el) { return $(el).off(".draghover"); }, _ZoneClass: { ACTIVE: "shiny-file-input-active", OVER: "shiny-file-input-over" }, _enableDocumentEvents: function _enableDocumentEvents() { var _this2 = this; var $doc = $("html"), _this$_ZoneClass = this._ZoneClass, ACTIVE = _this$_ZoneClass.ACTIVE, OVER = _this$_ZoneClass.OVER; this._enableDraghover($doc).on({ "draghover:enter.draghover": function draghoverEnterDraghover(e) { _this2._zoneOf($fileInputs).addClass(ACTIVE); }, "draghover:leave.draghover": function draghoverLeaveDraghover(e) { _this2._zoneOf($fileInputs).removeClass(ACTIVE); }, "draghover:drop.draghover": function draghoverDropDraghover(e) { _this2._zoneOf($fileInputs).removeClass(OVER).removeClass(ACTIVE); } }); }, _disableDocumentEvents: function _disableDocumentEvents() { var $doc = $("html"); $doc.off(".draghover"); this._disableDraghover($doc); }, _canSetFiles: function _canSetFiles(fileList) { var testEl = document.createElement("input"); testEl.type = "file"; try { testEl.files = fileList; } catch (e) { return false; } return true; }, _handleDrop: function _handleDrop(e, el) { var files = e.originalEvent.dataTransfer.files, $el = $(el); if (files === undefined || files === null) { // 1. The FileList object isn't supported by this browser, and // there's nothing else we can try. (< IE 10) console.log("Dropping files is not supported on this browser. (no FileList)"); } else if (!this._canSetFiles(files)) { // 2. The browser doesn't support assigning a type=file input's .files // property, but we do have a FileList to work with. (IE10+/Edge) $el.val(""); uploadDroppedFilesIE10Plus(el, files); } else { // 3. The browser supports FileList and input.files assignment. // (Chrome, Safari) $el.val(""); el.files = e.originalEvent.dataTransfer.files; // Recent versions of Firefox (57+, or "Quantum" and beyond) don't seem to // automatically trigger a change event, so we trigger one manually here. // On browsers that do trigger change, this operation appears to be // idempotent, as el.files doesn't change between events. $el.trigger("change"); } }, _isIE9: function _isIE9() { try { return window.navigator.userAgent.match(/MSIE 9\./) && true || false; } catch (e) { return false; } }, subscribe: function subscribe(el, callback) { var _this3 = this; $(el).on("change.fileInputBinding", uploadFiles); // Here we try to set up the necessary events for Drag and Drop ("DnD") on // every browser except IE9. We specifically exclude IE9 because it's one // browser that supports just enough of the functionality we need to be // confusing. In particular, it supports drag events, so drop zones will // highlight when a file is dragged into the browser window. It doesn't // support the FileList object though, so the user's expectation that DnD is // supported based on this highlighting would be incorrect. if (!this._isIE9()) { if ($fileInputs.length === 0) this._enableDocumentEvents(); $fileInputs = $fileInputs.add(el); var $zone = this._zoneOf(el), OVER = this._ZoneClass.OVER; this._enableDraghover($zone).on({ "draghover:enter.draghover": function draghoverEnterDraghover(e) { $zone.addClass(OVER); }, "draghover:leave.draghover": function draghoverLeaveDraghover(e) { $zone.removeClass(OVER); // Prevent this event from bubbling to the document handler, // which would deactivate all zones. e.stopPropagation(); }, "draghover:drop.draghover": function draghoverDropDraghover(e, dropEvent) { _this3._handleDrop(dropEvent, el); } }); } }, unsubscribe: function unsubscribe(el) { var $el = $(el), $zone = this._zoneOf(el); $zone.removeClass(this._ZoneClass.OVER).removeClass(this._ZoneClass.ACTIVE); this._disableDraghover($zone); $el.off(".fileInputBinding"); $zone.off(".draghover"); // Remove el from list of inputs and (maybe) clean up global event handlers. $fileInputs = $fileInputs.not(el); if ($fileInputs.length === 0) this._disableDocumentEvents(); } }); inputBindings.register(fileInputBinding, 'shiny.fileInputBinding'); //--------------------------------------------------------------------- // Source file: ../srcjs/init_shiny.js function initShiny() { var shinyapp = exports.shinyapp = new ShinyApp(); function bindOutputs() { var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; scope = $(scope); var bindings = outputBindings.getBindings(); for (var i = 0; i < bindings.length; i++) { var binding = bindings[i].binding; var matches = binding.find(scope) || []; for (var j = 0; j < matches.length; j++) { var el = matches[j]; var id = binding.getId(el); // Check if ID is falsy if (!id) continue; // In some uncommon cases, elements that are later in the // matches array can be removed from the document by earlier // iterations. See https://github.com/rstudio/shiny/issues/1399 if (!$.contains(document, el)) continue; var $el = $(el); if ($el.hasClass('shiny-bound-output')) { // Already bound; can happen with nested uiOutput (bindAll // gets called on two ancestors) continue; } var bindingAdapter = new OutputBindingAdapter(el, binding); shinyapp.bindOutput(id, bindingAdapter); $el.data('shiny-output-binding', bindingAdapter); $el.addClass('shiny-bound-output'); $el.trigger({ type: 'shiny:bound', binding: binding, bindingType: 'output' }); } } // Send later in case DOM layout isn't final yet. setTimeout(sendImageSize, 0); setTimeout(sendOutputHiddenState, 0); } function unbindOutputs() { var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var outputs = $(scope).find('.shiny-bound-output'); if (includeSelf && $(scope).hasClass('shiny-bound-output')) { outputs.push(scope); } for (var i = 0; i < outputs.length; i++) { var $el = $(outputs[i]); var bindingAdapter = $el.data('shiny-output-binding'); if (!bindingAdapter) continue; var id = bindingAdapter.binding.getId(outputs[i]); shinyapp.unbindOutput(id, bindingAdapter); $el.removeClass('shiny-bound-output'); $el.removeData('shiny-output-binding'); $el.trigger({ type: 'shiny:unbound', binding: bindingAdapter.binding, bindingType: 'output' }); } // Send later in case DOM layout isn't final yet. setTimeout(sendImageSize, 0); setTimeout(sendOutputHiddenState, 0); } var inputBatchSender = new InputBatchSender(shinyapp); var inputsNoResend = new InputNoResendDecorator(inputBatchSender); var inputsEvent = new InputEventDecorator(inputsNoResend); var inputsRate = new InputRateDecorator(inputsEvent); var inputsDefer = new InputDeferDecorator(inputsEvent); var inputs; if ($('input[type="submit"], button[type="submit"]').length > 0) { // If there is a submit button on the page, use defer decorator inputs = inputsDefer; $('input[type="submit"], button[type="submit"]').each(function () { $(this).click(function (event) { event.preventDefault(); inputsDefer.submit(); }); }); } else { // By default, use rate decorator inputs = inputsRate; } inputs = new InputValidateDecorator(inputs); exports.setInputValue = exports.onInputChange = function (name, value, opts) { opts = addDefaultInputOpts(opts); inputs.setInput(name, value, opts); }; var boundInputs = {}; function valueChangeCallback(binding, el, allowDeferred) { var id = binding.getId(el); if (id) { var value = binding.getValue(el); var type = binding.getType(el); if (type) id = id + ":" + type; var opts = { priority: allowDeferred ? "deferred" : "immediate", binding: binding, el: el }; inputs.setInput(id, value, opts); } } function bindInputs() { var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; var bindings = inputBindings.getBindings(); var inputItems = {}; for (var i = 0; i < bindings.length; i++) { var binding = bindings[i].binding; var matches = binding.find(scope) || []; for (var j = 0; j < matches.length; j++) { var el = matches[j]; var id = binding.getId(el); // Check if ID is falsy, or if already bound if (!id || boundInputs[id]) continue; var type = binding.getType(el); var effectiveId = type ? id + ":" + type : id; inputItems[effectiveId] = { value: binding.getValue(el), opts: { immediate: true, binding: binding, el: el } }; /*jshint loopfunc:true*/ var thisCallback = function () { var thisBinding = binding; var thisEl = el; return function (allowDeferred) { valueChangeCallback(thisBinding, thisEl, allowDeferred); }; }(); binding.subscribe(el, thisCallback); $(el).data('shiny-input-binding', binding); $(el).addClass('shiny-bound-input'); var ratePolicy = binding.getRatePolicy(el); if (ratePolicy !== null) { inputsRate.setRatePolicy(effectiveId, ratePolicy.policy, ratePolicy.delay); } boundInputs[id] = { binding: binding, node: el }; $(el).trigger({ type: 'shiny:bound', binding: binding, bindingType: 'input' }); } } return inputItems; } function unbindInputs() { var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var inputs = $(scope).find('.shiny-bound-input'); if (includeSelf && $(scope).hasClass('shiny-bound-input')) { inputs.push(scope); } for (var i = 0; i < inputs.length; i++) { var el = inputs[i]; var binding = $(el).data('shiny-input-binding'); if (!binding) continue; var id = binding.getId(el); $(el).removeClass('shiny-bound-input'); delete boundInputs[id]; binding.unsubscribe(el); $(el).trigger({ type: 'shiny:unbound', binding: binding, bindingType: 'input' }); } } function _bindAll(scope) { bindOutputs(scope); return bindInputs(scope); } function unbindAll(scope) { var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; unbindInputs(scope, includeSelf); unbindOutputs(scope, includeSelf); } exports.bindAll = function (scope) { // _bindAll returns input values; it doesn't send them to the server. // export.bindAll needs to send the values to the server. var currentInputItems = _bindAll(scope); $.each(currentInputItems, function (name, item) { inputs.setInput(name, item.value, item.opts); }); // Not sure if the iframe stuff is an intrinsic part of bindAll, but bindAll // is a convenient place to hang it. bindAll will be called anytime new HTML // appears that might contain inputs/outputs; it's reasonable to assume that // any such HTML may contain iframes as well. initDeferredIframes(); }; exports.unbindAll = unbindAll; // Calls .initialize() for all of the input objects in all input bindings, // in the given scope. function initializeInputs() { var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; var bindings = inputBindings.getBindings(); // Iterate over all bindings for (var i = 0; i < bindings.length; i++) { var binding = bindings[i].binding; var inputObjects = binding.find(scope) || []; // Iterate over all input objects for this binding for (var j = 0; j < inputObjects.length; j++) { if (!inputObjects[j]._shiny_initialized) { inputObjects[j]._shiny_initialized = true; binding.initialize(inputObjects[j]); } } } } exports.initializeInputs = initializeInputs; function getIdFromEl(el) { var $el = $(el); var bindingAdapter = $el.data("shiny-output-binding"); if (!bindingAdapter) return null;else return bindingAdapter.getId(); } // Initialize all input objects in the document, before binding initializeInputs(document); // The input values returned by _bindAll() each have a structure like this: // { value: 123, opts: { ... } } // We want to only keep the value. This is because when the initialValues is // passed to ShinyApp.connect(), the ShinyApp object stores the // initialValues object for the duration of the session, and the opts may // have a reference to the DOM element, which would prevent it from being // GC'd. var initialValues = mapValues(_bindAll(document), function (x) { return x.value; }); // The server needs to know the size of each image and plot output element, // in case it is auto-sizing $('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () { var id = getIdFromEl(this); if (this.offsetWidth !== 0 || this.offsetHeight !== 0) { initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth; initialValues['.clientdata_output_' + id + '_height'] = this.offsetHeight; } }); function getComputedBgColor(el) { if (!el) { // Top of document, can't recurse further return null; } var bgColor = getStyle(el, "background-color"); var m = bgColor.match(/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/); if (bgColor === "transparent" || m && parseFloat(m[4]) === 0) { // No background color on this element. See if it has a background image. var bgImage = getStyle(el, "background-image"); if (bgImage && bgImage !== "none") { // Failed to detect background color, since it has a background image return null; } else { // Recurse return getComputedBgColor(el.parentElement); } } return bgColor; } // Compute the color property of an a tag, scoped within the element function getComputedLinkColor(el) { var a = document.createElement("a"); a.href = "/"; var div = document.createElement("div"); div.style.setProperty("position", "absolute", "important"); div.style.setProperty("top", "-1000px", "important"); div.style.setProperty("left", "0", "important"); div.style.setProperty("width", "30px", "important"); div.style.setProperty("height", "10px", "important"); div.appendChild(a); el.appendChild(div); var linkColor = getStyle(a, "color"); el.removeChild(div); return linkColor; } function getComputedFont(el) { var fontFamily = getStyle(el, "font-family"); var fontSize = getStyle(el, "font-size"); return { families: fontFamily.replace(/"/g, '').split(", "), size: fontSize }; } $('.shiny-image-output, .shiny-plot-output, .shiny-report-theme').each(function () { var id = getIdFromEl(this); initialValues['.clientdata_output_' + id + '_bg'] = getComputedBgColor(this); initialValues['.clientdata_output_' + id + '_fg'] = getStyle(this, "color"); initialValues['.clientdata_output_' + id + '_accent'] = getComputedLinkColor(this); initialValues['.clientdata_output_' + id + '_font'] = getComputedFont(this); }); function doSendImageSize() { $('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () { var id = getIdFromEl(this); if (this.offsetWidth !== 0 || this.offsetHeight !== 0) { inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth); inputs.setInput('.clientdata_output_' + id + '_height', this.offsetHeight); } }); $('.shiny-image-output, .shiny-plot-output, .shiny-report-theme').each(function () { var id = getIdFromEl(this); inputs.setInput('.clientdata_output_' + id + '_bg', getComputedBgColor(this)); inputs.setInput('.clientdata_output_' + id + '_fg', getStyle(this, "color")); inputs.setInput('.clientdata_output_' + id + '_accent', getComputedLinkColor(this)); inputs.setInput('.clientdata_output_' + id + '_font', getComputedFont(this)); }); $('.shiny-bound-output').each(function () { var $this = $(this), binding = $this.data('shiny-output-binding'); $this.trigger({ type: 'shiny:visualchange', visible: !isHidden(this), binding: binding }); binding.onResize(); }); } var sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0); function sendImageSize() { sendImageSizeDebouncer.normalCall(); } // Make sure sendImageSize actually gets called before the inputBatchSender // sends data to the server. inputBatchSender.lastChanceCallback.push(function () { if (sendImageSizeDebouncer.isPending()) sendImageSizeDebouncer.immediateCall(); }); // Return true if the object or one of its ancestors in the DOM tree has // style='display:none'; otherwise return false. function isHidden(obj) { // null means we've hit the top of the tree. If width or height is // non-zero, then we know that no ancestor has display:none. if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) { return false; } else if (getStyle(obj, 'display') === 'none') { return true; } else { return isHidden(obj.parentNode); } } var lastKnownVisibleOutputs = {}; // Set initial state of outputs to hidden, if needed $('.shiny-bound-output').each(function () { var id = getIdFromEl(this); if (isHidden(this)) { initialValues['.clientdata_output_' + id + '_hidden'] = true; } else { lastKnownVisibleOutputs[id] = true; initialValues['.clientdata_output_' + id + '_hidden'] = false; } }); // Send update when hidden state changes function doSendOutputHiddenState() { var visibleOutputs = {}; $('.shiny-bound-output').each(function () { var id = getIdFromEl(this); delete lastKnownVisibleOutputs[id]; // Assume that the object is hidden when width and height are 0 var hidden = isHidden(this), evt = { type: 'shiny:visualchange', visible: !hidden }; if (hidden) { inputs.setInput('.clientdata_output_' + id + '_hidden', true); } else { visibleOutputs[id] = true; inputs.setInput('.clientdata_output_' + id + '_hidden', false); } var $this = $(this); evt.binding = $this.data('shiny-output-binding'); $this.trigger(evt); }); // Anything left in lastKnownVisibleOutputs is orphaned for (var name in lastKnownVisibleOutputs) { if (lastKnownVisibleOutputs.hasOwnProperty(name)) inputs.setInput('.clientdata_output_' + name + '_hidden', true); } // Update the visible outputs for next time lastKnownVisibleOutputs = visibleOutputs; } // sendOutputHiddenState gets called each time DOM elements are shown or // hidden. This can be in the hundreds or thousands of times at startup. // We'll debounce it, so that we do the actual work once per tick. var sendOutputHiddenStateDebouncer = new Debouncer(null, doSendOutputHiddenState, 0); function sendOutputHiddenState() { sendOutputHiddenStateDebouncer.normalCall(); } // We need to make sure doSendOutputHiddenState actually gets called before // the inputBatchSender sends data to the server. The lastChanceCallback // here does that - if the debouncer has a pending call, flush it. inputBatchSender.lastChanceCallback.push(function () { if (sendOutputHiddenStateDebouncer.isPending()) sendOutputHiddenStateDebouncer.immediateCall(); }); // Given a namespace and a handler function, return a function that invokes // the handler only when e's namespace matches. For example, if the // namespace is "bs", it would match when e.namespace is "bs" or "bs.tab". // If the namespace is "bs.tab", it would match for "bs.tab", but not "bs". function filterEventsByNamespace(namespace, handler) { namespace = namespace.split("."); return function (e) { var eventNamespace = e.namespace.split("."); // If any of the namespace strings aren't present in this event, quit. for (var i = 0; i < namespace.length; i++) { if (eventNamespace.indexOf(namespace[i]) === -1) return; } handler.apply(this, arguments); }; } // The size of each image may change either because the browser window was // resized, or because a tab was shown/hidden (hidden elements report size // of 0x0). It's OK to over-report sizes because the input pipeline will // filter out values that haven't changed. $(window).resize(debounce(500, sendImageSize)); // Need to register callbacks for each Bootstrap 3 class. var bs3classes = ['modal', 'dropdown', 'tab', 'tooltip', 'popover', 'collapse']; $.each(bs3classes, function (idx, classname) { $(document.body).on('shown.bs.' + classname + '.sendImageSize', '*', filterEventsByNamespace('bs', sendImageSize)); $(document.body).on('shown.bs.' + classname + '.sendOutputHiddenState ' + 'hidden.bs.' + classname + '.sendOutputHiddenState', '*', filterEventsByNamespace('bs', sendOutputHiddenState)); }); // This is needed for Bootstrap 2 compatibility and for non-Bootstrap // related shown/hidden events (like conditionalPanel) $(document.body).on('shown.sendImageSize', '*', sendImageSize); $(document.body).on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*', sendOutputHiddenState); // Send initial pixel ratio, and update it if it changes initialValues['.clientdata_pixelratio'] = pixelRatio(); $(window).resize(function () { inputs.setInput('.clientdata_pixelratio', pixelRatio()); }); // Send initial URL initialValues['.clientdata_url_protocol'] = window.location.protocol; initialValues['.clientdata_url_hostname'] = window.location.hostname; initialValues['.clientdata_url_port'] = window.location.port; initialValues['.clientdata_url_pathname'] = window.location.pathname; // Send initial URL search (query string) and update it if it changes initialValues['.clientdata_url_search'] = window.location.search; $(window).on('pushstate', function (e) { inputs.setInput('.clientdata_url_search', window.location.search); }); $(window).on('popstate', function (e) { inputs.setInput('.clientdata_url_search', window.location.search); }); // This is only the initial value of the hash. The hash can change, but // a reactive version of this isn't sent because watching for changes can // require polling on some browsers. The JQuery hashchange plugin can be // used if this capability is important. initialValues['.clientdata_url_hash_initial'] = window.location.hash; initialValues['.clientdata_url_hash'] = window.location.hash; $(window).on('hashchange', function (e) { inputs.setInput('.clientdata_url_hash', window.location.hash); }); // The server needs to know what singletons were rendered as part of // the page loading var singletonText = initialValues['.clientdata_singletons'] = $('script[type="application/shiny-singletons"]').text(); singletons.registerNames(singletonText.split(/,/)); var dependencyText = $('script[type="application/html-dependencies"]').text(); $.each(dependencyText.split(/;/), function (i, depStr) { var match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr); if (match) { registerDependency(match[1], match[2]); } }); // IE8 and IE9 have some limitations with data URIs initialValues['.clientdata_allowDataUriScheme'] = typeof WebSocket !== 'undefined'; // We've collected all the initial values--start the server process! inputsNoResend.reset(initialValues); shinyapp.connect(initialValues); $(document).one("shiny:connected", function () { initDeferredIframes(); }); } // function initShiny() // Give any deferred iframes a chance to load. function initDeferredIframes() { if (!window.Shiny || !window.Shiny.shinyapp || !window.Shiny.shinyapp.isConnected()) { // If somehow we accidentally call this before the server connection is // established, just ignore the call. At the time of this writing it // doesn't happen, but it's easy to imagine a later refactoring putting // us in this situation and it'd be hard to notice with either manual // testing or automated tests, because the only effect is on HTTP request // timing. (Update: Actually Aron saw this being called without even // window.Shiny being defined, but it was hard to repro.) return; } $(".shiny-frame-deferred").each(function (i, el) { var $el = $(el); $el.removeClass("shiny-frame-deferred"); $el.attr("src", $el.attr("data-deferred-src")); $el.attr("data-deferred-src", null); }); } $(function () { // Init Shiny a little later than document ready, so user code can // run first (i.e. to register bindings) setTimeout(initShiny, 1); }); //--------------------------------------------------------------------- // Source file: ../srcjs/reactlog.js $(document).on('keydown', function (e) { if (e.which !== 114 || !e.ctrlKey && !e.metaKey || e.shiftKey || e.altKey) return; var url = 'reactlog?w=' + window.escape(exports.shinyapp.config.workerId) + "&s=" + window.escape(exports.shinyapp.config.sessionId); window.open(url); e.preventDefault(); }); $(document).on('keydown', function (e) { if ( // if not one of the key combos below !( // cmd/ctrl + fn + f4 e.which === 115 && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey || // cmd/ctrl + shift + fn + f3 e.which === 114 && (e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey)) { return; } var url = 'reactlog/mark?w=' + window.escape(exports.shinyapp.config.workerId) + "&s=" + window.escape(exports.shinyapp.config.sessionId); // send notification $.get(url, function (result) { if (result !== "marked") return; var html = '<span id="shiny-reactlog-mark-text">Marked time point in reactlog</span>'; exports.notifications.show({ html: html, closeButton: true }); }).fail(function () { // found returned error while marking, should open webpage window.open(url); }); e.preventDefault(); }); //--------------------------------------------------------------------- // Source file: ../srcjs/_end.js })(); //# sourceMappingURL=shiny.js.map //# sourceMappingURL=shiny.js.map