/*jshint undef:true, browser:true, devel: true, jquery:true, strict:false, curly:false, indent:2 */ (function() { var $ = jQuery; var exports = window.Shiny = window.Shiny || {}; var browser = {}; browser.isQt = false; // For easy handling of Qt quirks using CSS if (/\bQt\//.test(window.navigator.userAgent)) { $(document.documentElement).addClass('qt'); browser.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 browser.isIE = (navigator.appName === 'Microsoft Internet Explorer'); browser.IEVersion = getIEVersion(); function getIEVersion() { var rv = -1; if (browser.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; } $(document).on('submit', 'form:not([action])', function(e) { e.preventDefault(); }); $(document).on('click', 'a.action-button', function(e) { e.preventDefault(); }); // Escape jQuery selector metacharacters: !"#$%&'()*+,./:;<=>?@[\]^`{|}~ var $escape = exports.$escape = function(val) { return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1'); }; function escapeHTML(str) { return str.replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") .replace(/\//g,"/"); } function randomId() { return Math.floor(0x100000000 + (Math.random() * 0xF00000000)).toString(16); } function strToBool(str) { if (!str || !str.toLowerCase) return undefined; switch(str.toLowerCase()) { case 'true': return true; case 'false': return false; default: return undefined; } } // A wrapper for getComputedStyle that is compatible with older browsers. // This is significantly faster than jQuery's .css() function. function getStyle(el, styleProp) { var x; if (el.currentStyle) x = el.currentStyle[styleProp]; else if (window.getComputedStyle) { // getComputedStyle can return null when we're inside a hidden iframe on // Firefox; don't attempt to retrieve style props in this case. // https://bugzilla.mozilla.org/show_bug.cgi?id=548397 var style = document.defaultView.getComputedStyle(el, null); if (style) x = style.getPropertyValue(styleProp); } return x; } // Convert a number to a string with leading zeros function padZeros(n, digits) { var str = n.toString(); while (str.length < digits) str = "0" + str; return str; } // Take a string with format "YYYY-MM-DD" and return a Date object. // IE8 and QTWebKit don't support YYYY-MM-DD, but they support YYYY/MM/DD function parseDate(dateString) { var date = new Date(dateString); if (isNaN(date)) date = new Date(dateString.replace(/-/g, "/")); return date; } function slice(blob, start, end) { if (blob.slice) return blob.slice(start, end); if (blob.mozSlice) return blob.mozSlice(start, end); if (blob.webkitSlice) return blob.webkitSlice(start, end); throw "Blob doesn't support slice"; } // Given an element and a function(width, height), returns a function(). When // the output function is called, it calls the input function with the offset // width and height of the input element--but only if the size of the element // is non-zero and the size is different than the last time the output // function was called. // // Basically we are trying to filter out extraneous calls to func, so that // when the window size changes or whatever, we don't run resize logic for // elements that haven't actually changed size or aren't visible anyway. function makeResizeFilter(el, func) { var lastSize = {}; return function() { var size = { w: el.offsetWidth, h: el.offsetHeight }; if (size.w === 0 && size.h === 0) return; if (size.w === lastSize.w && size.h === lastSize.h) return; lastSize = size; func(size.w, size.h); }; } var _BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; function makeBlob(parts) { // Browser compatibility is a mess right now. The code as written works in // a variety of modern browsers, but sadly gives a deprecation warning // message on the console in current versions (as of this writing) of // Chrome. // Safari 6.0 (8536.25) on Mac OS X 10.8.1: // Has Blob constructor but it doesn't work with ArrayBufferView args // Google Chrome 21.0.1180.81 on Xubuntu 12.04: // Has Blob constructor, accepts ArrayBufferView args, accepts ArrayBuffer // but with a deprecation warning message // Firefox 15.0 on Xubuntu 12.04: // Has Blob constructor, accepts both ArrayBuffer and ArrayBufferView args // Chromium 18.0.1025.168 (Developer Build 134367 Linux) on Xubuntu 12.04: // No Blob constructor. Has WebKitBlobBuilder. try { return new Blob(parts); } catch (e) { var blobBuilder = new _BlobBuilder(); $.each(parts, function(i, part) { blobBuilder.append(part); }); return blobBuilder.getBlob(); } } function pixelRatio() { if (window.devicePixelRatio) { return window.devicePixelRatio; } else { return 1; } } // Takes a string expression and returns a function that takes an argument. // // When the function is executed, it will evaluate that expression using // "with" on the argument value, and return the result. function scopeExprToFunc(expr) { /*jshint evil: true */ var func = new Function("with (this) {return (" + expr + ");}"); return function(scope) { return func.call(scope); }; } // ========================================================================= // Input rate stuff // ========================================================================= var Invoker = function(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(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(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. 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(shinyapp) { this.shinyapp = shinyapp; this.timerId = null; this.pendingData = {}; this.reentrant = false; this.lastChanceCallback = []; }; (function() { this.setInput = function(name, value) { var self = this; this.pendingData[name] = value; if (!this.timerId && !this.reentrant) { this.timerId = setTimeout(function() { self.reentrant = true; try { $.each(self.lastChanceCallback, function(i, callback) { callback(); }); self.timerId = null; var currentData = self.pendingData; self.pendingData = {}; self.shinyapp.sendInput(currentData); } finally { self.reentrant = false; } }, 0); } }; }).call(InputBatchSender.prototype); var InputNoResendDecorator = function(target, initialValues) { this.target = target; this.lastSentValues = initialValues || {}; }; (function() { this.setInput = function(name, value) { var jsonValue = JSON.stringify(value); if (this.lastSentValues[name] === jsonValue) return; this.lastSentValues[name] = jsonValue; this.target.setInput(name, value); }; this.reset = function(values) { values = values || {}; var strValues = {}; $.each(values, function(key, value) { strValues[key] = JSON.stringify(value); }); this.lastSentValues = strValues; }; }).call(InputNoResendDecorator.prototype); var InputDeferDecorator = function(target) { this.target = target; this.pendingInput = {}; }; (function() { this.setInput = function(name, value) { if (/^\./.test(name)) this.target.setInput(name, value); else this.pendingInput[name] = value; }; this.submit = function() { for (var name in this.pendingInput) { if (this.pendingInput.hasOwnProperty(name)) this.target.setInput(name, this.pendingInput[name]); } }; }).call(InputDeferDecorator.prototype); var InputRateDecorator = function(target) { this.target = target; this.inputRatePolicies = {}; }; (function() { this.setInput = function(name, value, immediate) { this.$ensureInit(name); if (immediate) this.inputRatePolicies[name].immediateCall(name, value, immediate); else this.inputRatePolicies[name].normalCall(name, value, immediate); }; this.setRatePolicy = function(name, mode, millis) { if (mode === 'direct') { this.inputRatePolicies[name] = new Invoker(this, this.$doSetInput); } else if (mode === 'debounce') { this.inputRatePolicies[name] = new Debouncer(this, this.$doSetInput, millis); } else if (mode === 'throttle') { this.inputRatePolicies[name] = new Throttler(this, this.$doSetInput, millis); } }; this.$ensureInit = function(name) { if (!(name in this.inputRatePolicies)) this.setRatePolicy(name, 'direct'); }; this.$doSetInput = function(name, value) { this.target.setInput(name, value); }; }).call(InputRateDecorator.prototype); // 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; } // ========================================================================= // ShinyApp // ========================================================================= var ShinyApp = function() { this.$socket = null; // Cached input values this.$inputValues = {}; // Output bindings this.$bindings = {}; // Cached values/errors this.$values = {}; this.$errors = {}; // Conditional bindings (show/hide element based on expression) this.$conditionals = {}; this.$pendingMessages = []; this.$activeRequests = {}; this.$nextRequestId = 0; }; (function() { this.connect = function(initialInput) { if (this.$socket) throw "Connect was already called on this application object"; $.extend(initialInput, { // IE8 and IE9 have some limitations with data URIs ".clientdata_allowDataUriScheme": typeof WebSocket !== 'undefined' }); this.$socket = this.createSocket(); this.$initialInput = initialInput; $.extend(this.$inputValues, initialInput); this.$updateConditionals(); }; this.isConnected = function() { return !!this.$socket; }; 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(); socket.onopen = function() { 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); }; socket.onclose = function() { $(document.body).addClass('disconnected'); self.$notifyDisconnected(); }; return socket; }; this.sendInput = function(values) { var msg = JSON.stringify({ method: 'update', data: values }); this.$sendMsg(msg); $.extend(this.$inputValues, values); this.$updateConditionals(); }; this.$notifyDisconnected = function() { // function to normalize hostnames var normalize = function(hostname) { if (hostname == "127.0.0.1") return "localhost"; else return hostname; }; // Send a 'disconnected' message to parent if we are on the same domin var parentUrl = (parent !== window) ? document.referrer : null; if (parentUrl) { // parse the parent href var a = document.createElement('a'); a.href = parentUrl; // post the disconnected message if the hostnames are the same if (normalize(a.hostname) == normalize(window.location.hostname)) { var protocol = a.protocol.replace(':',''); // browser compatability var origin = protocol + '://' + a.hostname; if (a.port) origin = origin + ':' + a.port; parent.postMessage('disconnected', origin); } } }; // 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(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]; if (binding && binding.onValueError) { binding.onValueError(error); } }; this.receiveOutput = function(name, value) { if (this.$values[name] === value) return; this.$values[name] = value; delete this.$errors[name]; var binding = this.$bindings[name]; if (binding) { binding.onValueChange(value); } return value; }; this.bindOutput = function(id, binding) { if (!id) throw "Can't bind an element with no ID"; if (this.$bindings[id]) throw "Duplicate binding for ID " + id; this.$bindings[id] = binding; if (this.$values[id] !== undefined) binding.onValueChange(this.$values[id]); else if (this.$errors[id] !== undefined) binding.onValueError(this.$errors[id]); return binding; }; this.unbindOutput = function(id, binding) { if (this.$bindings[id] === binding) { delete this.$bindings[id]; return true; } else { return false; } }; this.$updateConditionals = function() { var inputs = {}; // Input keys use "name:type" format; we don't want the user to // have to know about the type suffix when referring to inputs. for (var name in this.$inputValues) { if (this.$inputValues.hasOwnProperty(name)) { var shortName = name.replace(/:.*/, ''); inputs[shortName] = this.$inputValues[name]; } } var scope = {input: inputs, output: this.$values}; var conditionals = $(document).find('[data-display-if]'); for (var i = 0; i < conditionals.length; i++) { var el = $(conditionals[i]); var condFunc = el.data('data-display-if-func'); if (!condFunc) { var condExpr = el.attr('data-display-if'); condFunc = scopeExprToFunc(condExpr); el.data('data-display-if-func', condFunc); } var show = condFunc(scope); var showing = el.css("display") !== "none"; if (show !== showing) { if (show) { el.trigger('show'); el.show(); el.trigger('shown'); } else { el.trigger('hide'); el.hide(); el.trigger('hidden'); } } } }; // Message handler management functions ================================= // Records insertion order of handlers. Maps number to name. This is so // we can dispatch messages to handlers in the order that handlers were // added. var messageHandlerOrder = []; // Keep track of handlers by name. Maps name to handler function. var messageHandlers = {}; // Two categories of message handlers: those that are from Shiny, and those // that are added by the user. The Shiny ones handle messages in // msgObj.values, msgObj.errors, and so on. The user ones handle messages // in msgObj.custom.foo and msgObj.custom.bar. var customMessageHandlerOrder = []; var customMessageHandlers = {}; // Adds Shiny (internal) message handler function addMessageHandler(type, handler) { if (messageHandlers[type]) { throw('handler for message of type "' + type + '" already added.'); } if (typeof(handler) !== 'function') { throw('handler must be a function.'); } if (handler.length !== 1) { throw('handler must be a function that takes one argument.'); } messageHandlerOrder.push(type); messageHandlers[type] = handler; } // Adds custom message handler - this one is exposed to the user function addCustomMessageHandler(type, handler) { if (customMessageHandlers[type]) { throw('handler for message of type "' + type + '" already added.'); } if (typeof(handler) !== 'function') { throw('handler must be a function.'); } if (handler.length !== 1) { throw('handler must be a function that takes one argument.'); } customMessageHandlerOrder.push(type); customMessageHandlers[type] = handler; } exports.addCustomMessageHandler = addCustomMessageHandler; this.dispatchMessage = function(msg) { var msgObj = JSON.parse(msg); // Send msgObj.foo and msgObj.bar to appropriate handlers this._sendMessagesToHandlers(msgObj, 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[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) { $(document.documentElement).removeClass('shiny-busy'); 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) { inputBinding.receiveMessage($obj[0], message[i].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('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('custom', function(message) { // For old-style custom messages - should deprecate and migrate to new // method if (exports.oncustommessage) { exports.oncustommessage(message); } // Send messages.foo and messages.bar to appropriate handlers this._sendMessagesToHandlers(message, customMessageHandlers, customMessageHandlerOrder); }); addMessageHandler('config', function(message) { this.config = message; }); // Progress reporting ==================================================== var progressHandlers = { // Progress for a particular object binding: function(message) { $(document.documentElement).addClass('shiny-busy'); var key = message.id; var binding = this.$bindings[key]; if (binding && binding.showProgress) { binding.showProgress(true); } }, // Open a page-level progress bar open: function(message) { // Add progress container (for all progress items) if not already present var $container = $('.shiny-progress-container'); if ($container.length === 0) { $container = $('
'); $('body').append($container); } // Add div for just this progress ID var depth = $('.shiny-progress.open').length; var $progress = $(progressHandlers.progressHTML); $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(message) { var $progress = $('#' + message.id + '.shiny-progress'); if (typeof(message.message) !== 'undefined') { $progress.find('.progress-message').text(message.message); } if (typeof(message.detail) !== 'undefined') { $progress.find('.progress-detail').text(message.detail); } if (typeof(message.value) !== 'undefined') { if (message.value !== null) { $progress.find('.progress').show(); $progress.find('.bar').width((message.value*100) + '%'); } else { $progress.find('.progress').hide(); } } $progress.fadeIn(); }, // Close page-level progress bar close: function(message) { var $progress = $('#' + message.id + '.shiny-progress'); $progress.removeClass('open'); $progress.fadeOut({ complete: function() { $progress.remove(); // If this was the last shiny-progress, remove container if ($('.shiny-progress').length === 0) $('.shiny-progress-container').remove(); } }); }, // The 'bar' class is needed for backward compatibility with Bootstrap 2. progressHTML: '
' + '
' + '
' + 'message' + '' + '
' + '
' }; exports.progressHandlers = progressHandlers; }).call(ShinyApp.prototype); // ========================================================================= // File Processor // ========================================================================= // 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(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() { var self = this; 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); // ========================================================================= // Binding registry // ========================================================================= var BindingRegistry = function() { 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(); // ========================================================================= // Output bindings // ========================================================================= 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); var textOutputBinding = new OutputBinding(); $.extend(textOutputBinding, { find: function(scope) { return $(scope).find('.shiny-text-output'); }, renderValue: function(el, data) { $(el).text(data); } }); outputBindings.register(textOutputBinding, 'shiny.textOutput'); var imageOutputBinding = new OutputBinding(); $.extend(imageOutputBinding, { find: function(scope) { return $(scope).find('.shiny-image-output, .shiny-plot-output'); }, renderValue: function(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 $el = $(el); // Load the image before emptying, to minimize flicker var img = null; // Remove event handlers that were added in previous renderValue() $el.off('.image_output'); // Trigger custom 'remove' event for any existing images in the div $el.find('img').trigger('remove'); if (!data) { $el.empty(); return; } var opts = { clickId: $el.data('click-id'), clickClip: strToBool($el.data('click-clip')) || true, dblclickId: $el.data('dblclick-id'), dblclickClip: strToBool($el.data('dblclick-clip')) || true, dblclickDelay: $el.data('dblclick-delay') || 400, hoverId: $el.data('hover-id'), hoverClip: $el.data('hover-clip') || true, hoverDelayType: $el.data('hover-delay-type') || 'debounce', hoverDelay: $el.data('hover-delay') || 300, brushId: $el.data('brush-id'), brushClip: strToBool($el.data('brush-clip')) || true, brushDelayType: $el.data('brush-delay-type') || 'debounce', brushDelay: $el.data('brush-delay') || 300, brushFill: $el.data('brush-fill') || '#666', brushStroke: $el.data('brush-stroke') || '#000', brushOpacity: $el.data('brush-opacity') || 0.3, brushDirection: $el.data('brush-direction') || 'xy', brushResetOnNew: strToBool($el.data('brush-reset-on-new')) || false, coordmap: data.coordmap }; img = document.createElement('img'); // Copy items from data to img. This should include 'src' $.each(data, function(key, value) { if (value !== null) img[key] = value; }); var $img = $(img); // Firefox doesn't have offsetX/Y, so we need to use an alternate // method of calculation for it. Even though other browsers do have // offsetX/Y, we need to calculate relative to $el, because sometimes the // mouse event can come with offset relative to other elements on the // page. This happens when the event listener is bound to, say, window. function mouseOffset(mouseEvent) { var offset = $el.offset(); return { x: mouseEvent.pageX - offset.left, y: mouseEvent.pageY - offset.top }; } // Transform offset coordinates to data space coordinates function offsetToScaledCoords(offset, clip) { // By default, clip to plotting region clip = clip || true; var coordmap = opts.coordmap; if (!coordmap) return offset; function devToUsrX(deviceX) { var x = deviceX - coordmap.bounds.left; var factor = (coordmap.usr.right - coordmap.usr.left) / (coordmap.bounds.right - coordmap.bounds.left); var newx = (x * factor) + coordmap.usr.left; if (clip) { var max = Math.max(coordmap.usr.right, coordmap.usr.left); var min = Math.min(coordmap.usr.right, coordmap.usr.left); if (newx > max) newx = max; else if (newx < min) newx = min; } return newx; } function devToUsrY(deviceY) { var y = deviceY - coordmap.bounds.bottom; var factor = (coordmap.usr.top - coordmap.usr.bottom) / (coordmap.bounds.top - coordmap.bounds.bottom); var newy = (y * factor) + coordmap.usr.bottom; if (clip) { var max = Math.max(coordmap.usr.top, coordmap.usr.bottom); var min = Math.min(coordmap.usr.top, coordmap.usr.bottom); if (newy > max) newy = max; else if (newy < min) newy = min; } return newy; } var userX = devToUsrX(offset.x); if (coordmap.log.x) userX = Math.pow(10, userX); var userY = devToUsrY(offset.y); if (coordmap.log.y) userY = Math.pow(10, userY); return { x: userX, y: userY }; } // Get the pixel bounds of the coordmap; if there's no coordmap, return // the bounds of the image. function getPlotBounds() { if (opts.coordmap) { return opts.coordmap.bounds; } else { return { top: 0, left: 0, right: img.clientWidth - 1, bottom: img.clientHeight - 1 }; } } // Is an offset in the plotting region? If supplied, `expand` tells us to // expand the region by that many pixels in all directions. function isInPlottingRegion(offset, expand) { expand = expand || 0; var bounds = getPlotBounds(); return offset.x < bounds.right + expand && offset.x > bounds.left - expand && offset.y < bounds.bottom + expand && offset.y > bounds.top - expand; } // Given an offset, clip it to the plotting region as specified by // coordmap. If there is no coordmap, clip it to bounds of the DOM // element. function clipToPlottingRegion(offset) { var bounds = getPlotBounds(); var newOffset = { x: offset.x, y: offset.y }; if (offset.x > bounds.right) newOffset.x = bounds.right; else if (offset.x < bounds.left) newOffset.x = bounds.left; if (offset.y > bounds.bottom) newOffset.y = bounds.bottom; else if (offset.y < bounds.top) newOffset.y = bounds.top; return newOffset; } // Returns a function that sends mouse coordinates, scaled to data space. // If that function is passed a null event, it will send null. function mouseCoordinateSender(inputId, clip) { clip = clip || true; return function(e) { if (e === null) { exports.onInputChange(inputId, null); return; } var offset = mouseOffset(e); // Ignore events outside of plotting region if (clip && !isInPlottingRegion(offset)) return; var coords = offsetToScaledCoords(offset); coords[".nonce"] = Math.random(); exports.onInputChange(inputId, coords); }; } // ---------------------------------------------------------- // 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'). // ---------------------------------------------------------- function createClickHandler(inputId) { var clickInfoSender = mouseCoordinateSender(inputId, opts.clickClip); return { mousedown: function(e) { // Listen for left mouse button only if (e.which !== 1) return; clickInfoSender(e); }, onRemoveImg: function() { clickInfoSender(null); } }; } function createHoverHandler(inputId) { var sendHoverInfo = mouseCoordinateSender(inputId, opts.hoverClip); var hoverInfoSender; if (opts.hoverDelayType === 'throttle') hoverInfoSender = new Throttler(null, sendHoverInfo, opts.hoverDelay); else hoverInfoSender = new Debouncer(null, sendHoverInfo, opts.hoverDelay); return { mousemove: function(e) { hoverInfoSender.normalCall(e); }, onRemoveImg: function() { hoverInfoSender.immediateCall(null); } }; } // Returns a brush handler object. This has three public functions: // mousedown, mousemove, and onRemoveImg. function createBrushHandler(inputId) { // Parameter: expand the area in which a brush can be started, by this // many pixels in all directions. var expandPixels = 20; // Object that encapsulates brush state var brush = { // Current brushing and dragging state brushing: false, dragging: false, // Offset of last mouse down and up events down: { x: NaN, y: NaN }, up: { x: NaN, y: NaN }, // Bounding rectangle of the brush bounds: { xmin: NaN, xmax: NaN, ymin: NaN, ymax: NaN }, // The bounds at the start of a drag dragStartBounds: { xmin: NaN, xmax: NaN, ymin: NaN, ymax: NaN }, // div that displays the brush $div: null, reset: function() { this.brushing = false; this.dragging = false; this.down = { x: NaN, y: NaN }; this.up = { x: NaN, y: NaN }; this.bounds = { xmin: NaN, xmax: NaN, ymin: NaN, ymax: NaN }; this.dragStartBounds = { xmin: NaN, xmax: NaN, ymin: NaN, ymax: NaN }; if (this.$div) this.$div.remove(); return this; }, // If there's an existing brush div, use that div to set the new // brush's settings. importOldBrush: function() { var oldDiv = $el.find('#' + el.id + '_brush'); if (oldDiv.length === 0) return; var elOffset = $el.offset(); var divOffset = oldDiv.offset(); this.bounds = { xmin: divOffset.left - elOffset.left, xmax: divOffset.left - elOffset.left + oldDiv.width(), ymin: divOffset.top - elOffset.top, ymax: divOffset.top - elOffset.top + oldDiv.height() }; this.$div = oldDiv; }, // Return true if the offset is inside min/max coords isInsideBrush: function(offset) { var bounds = this.bounds; return offset.x <= bounds.xmax && offset.x >= bounds.xmin && offset.y <= bounds.ymax && offset.y >= bounds.ymin; }, // Sets the bounds of the brush, given a bounding box. This knows // whether we're brushing in the x, y, or xy directions and sets // bounds accordingly. setBounds: function(box) { var plotBounds = getPlotBounds(); var min = { x: box.xmin, y: box.ymin }; var max = { x: box.xmax, y: box.ymax }; if (opts.brushClip) { min = clipToPlottingRegion(min); max = clipToPlottingRegion(max); } if (opts.brushDirection === 'xy') { // No change } else if (opts.brushDirection === 'x') { // Extend top and bottom of plotting area min.y = plotBounds.top; max.y = plotBounds.bottom; } else if (opts.brushDirection === 'y') { min.x = plotBounds.left; max.x = plotBounds.right; } this.bounds = { xmin: min.x, xmax: max.x, ymin: min.y, ymax: max.y }; }, // Add a new div representing the brush. addDiv: function() { if (this.$div) this.$div.remove(); this.$div = $(document.createElement('div')) .attr('id', el.id + '_brush') .css({ 'background-color': opts.brushFill, 'opacity': opts.brushOpacity, 'pointer-events': 'none', 'position': 'absolute' }); var borderStyle = '1px solid ' + opts.brushStroke; if (opts.brushDirection === 'xy') { this.$div.css({ 'border': borderStyle }); } else if (opts.brushDirection === 'x') { this.$div.css({ 'border-left': borderStyle, 'border-right': borderStyle }); } else if (opts.brushDirection === 'y') { this.$div.css({ 'border-top': borderStyle, 'border-bottom': borderStyle }); } $el.append(this.$div); this.$div.offset({x:0, y:0}).width(0).height(0).show(); }, // Update the brush div to reflect the current brush bounds. updateDiv: function() { // Need parent offset relative to page to calculate mouse offset // relative to page. var imgOffset = $el.offset(); var b = this.bounds; this.$div.offset({ top: imgOffset.top + b.ymin, left: imgOffset.left + b.xmin }) .width(b.xmax - b.xmin) .height(b.ymax - b.ymin) .show(); }, startBrushing: function() { this.brushing = true; this.addDiv(); this.setBounds(findBox(this.down, this.down)); this.updateDiv(); }, brushTo: function(offset) { this.setBounds(findBox(this.down, offset)); this.updateDiv(); }, stopBrushing: function() { this.brushing = false; // Save the final bounding box of the brush this.setBounds(findBox(this.down, this.up)); }, startDragging: function() { this.dragging = true; this.dragStartBounds = $.extend({}, this.bounds); }, dragTo: function(offset) { // How far the brush was dragged var dx = offset.x - this.down.x; var dy = offset.y - this.down.y; // Calculate what new start/end positions would be, before clipping. var start = this.dragStartBounds; var newBounds = { xmin: start.xmin + dx, xmax: start.xmax + dx, ymin: start.ymin + dy, ymax: start.ymax + dy }; // Clip to the plotting area if (opts.brushClip) { var plotBounds = getPlotBounds(); // Convert to format for shiftToRange var xvals = [ newBounds.xmin, newBounds.xmax ]; var yvals = [ newBounds.ymin, newBounds.ymax ]; xvals = shiftToRange(xvals, plotBounds.left, plotBounds.right); yvals = shiftToRange(yvals, plotBounds.top, plotBounds.bottom); // Convert back to bounds format newBounds = { xmin: xvals[0], xmax: xvals[1], ymin: yvals[0], ymax: yvals[1] }; } this.setBounds(newBounds); this.updateDiv(); }, stopDragging: function() { this.dragging = false; } }; // 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). function findBox(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. function shiftToRange(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 2 || Math.abs(this.pending_e.offsetY - e.offsetY) > 2) { this.triggerPendingMousedown2(); this.scheduleMousedown2(e); } else { // The second click was close to the first one. If it happened // within specified delay, trigger our custom 'dblclick2' event. this.pending_e = null; this.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. dblclickIE8: function(e) { e.which = 1; // In IE8, e.which is 0 instead of 1. ??? this.triggerEvent('dblclick2', e); } }; $el.on('mousedown.image_output', function(e) { clickInfo.mousedown(e); }); if (browser.isIE && browser.IEVersion === 8) { $el.on('dblclick.image_output', function(e) { clickInfo.dblclickIE8(e); }); } // ---------------------------------------------------------- // Register the various event handlers // ---------------------------------------------------------- if (opts.clickId) { var clickHandler = createClickHandler(opts.clickId); $el.on('mousedown2.image_output', clickHandler.mousedown); // When img is removed, do housekeeping: clear $el's mouse listener and // call the handler's onRemoveImg callback. $img.on('remove', clickHandler.onRemoveImg); } if (opts.dblclickId) { // We'll use the clickHandler's mousedown function, but register it to // our custom 'dblclick2' event. var dblclickHandler = createClickHandler(opts.dblclickId); $el.on('dblclick2.image_output', dblclickHandler.mousedown); $img.on('remove', dblclickHandler.onRemoveImg); } if (opts.hoverId) { var hoverHandler = createHoverHandler(opts.hoverId); $el.on('mousemove.image_output', hoverHandler.mousemove); $img.on('remove', hoverHandler.onRemoveImg); } if (opts.brushId) { // Make image non-draggable (Chrome, Safari) $img.css('-webkit-user-drag', 'none'); // Firefox, IE<=10 $img.on('dragstart', function() { return false; }); // Disable selection of image and text when dragging in IE<=10 $el.on('selectstart.image_output', function() { return false; }); var brushHandler = createBrushHandler(opts.brushId); $el.on('mousedown.image_output', brushHandler.mousedown); $el.on('mousemove.image_output', brushHandler.mousemove); $img.on('remove', brushHandler.onRemoveImg); } if (opts.clickId || opts.dblclickId || opts.hoverId || opts.brushId) { $el.addClass('crosshair'); } $el.find('img').remove(); if (img) $el.append(img); } }); outputBindings.register(imageOutputBinding, 'shiny.imageOutput'); var htmlOutputBinding = new OutputBinding(); $.extend(htmlOutputBinding, { find: function(scope) { return $(scope).find('.shiny-html-output'); }, onValueError: function(el, err) { exports.unbindAll(el); this.renderError(el, err); }, renderValue: function(el, data) { exports.unbindAll(el); var html; var dependencies = []; if (data === null) { html = ''; } else if (typeof(data) === 'string') { html = data; } else if (typeof(data) === 'object') { html = data.html; dependencies = data.deps; } exports.renderHtml(html, el, dependencies); exports.initializeInputs(el); exports.bindAll(el); } }); 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, inserting singletons into head as needed exports.renderHtml = function(html, el, dependencies) { renderDependencies(dependencies); return singletons.renderHtml(html, el); }; function asArray(value) { if (value === null) return []; if ($.isArray(value)) return value; return [value]; } var htmlDependencies = {}; function registerDependency(name, version) { htmlDependencies[name] = version; } // Client-side dependency resolution and rendering function renderDependency(dep) { if (htmlDependencies.hasOwnProperty(dep.name)) return false; registerDependency(dep.name, dep.version); var href = dep.src.href; var $head = $("head").first(); if (dep.meta) { var metas = $.map(asArray(dep.meta), function(content, name) { return $("").attr("name", name).attr("content", content); }); $head.append(metas); } if (dep.stylesheet) { var stylesheets = $.map(asArray(dep.stylesheet), function(stylesheet) { return $("") .attr("href", href + "/" + encodeURI(stylesheet)); }); $head.append(stylesheets); } if (dep.script) { var scripts = $.map(asArray(dep.script), function(scriptName) { return $("