diff --git a/inst/www/shared/shiny.js b/inst/www/shared/shiny.js index 84beebeaf..d12ad76a7 100644 --- a/inst/www/shared/shiny.js +++ b/inst/www/shared/shiny.js @@ -24,205 +24,205 @@ //--------------------------------------------------------------------- // Source file: ../srcjs/utils.js - function escapeHTML(str) { - return str.replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/\//g,"/"); - } +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 randomId() { + return Math.floor(0x100000000 + (Math.random() * 0xF00000000)).toString(16); +} - function strToBool(str) { - if (!str || !str.toLowerCase) +function strToBool(str) { + if (!str || !str.toLowerCase) + return undefined; + + switch(str.toLowerCase()) { + case 'true': + return true; + case 'false': + return false; + default: 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; +// 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; - } +// 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; - } +// 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 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 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"; - } - - function pixelRatio() { - if (window.devicePixelRatio) { - return window.devicePixelRatio; - } else { - return 1; - } - } - - // Takes a string expression and returns a function that takes an argument. - // - // When the function is executed, it will evaluate that expression using - // "with" on the argument value, and return the result. - function scopeExprToFunc(expr) { - /*jshint evil: true */ - var func = new Function("with (this) {return (" + expr + ");}"); - return function(scope) { - return func.call(scope); - }; - } - - function asArray(value) { - if (value === null) - 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'); +// 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 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"; +} + +function pixelRatio() { + if (window.devicePixelRatio) { + return window.devicePixelRatio; + } else { + return 1; + } +} + +// Takes a string expression and returns a function that takes an argument. +// +// When the function is executed, it will evaluate that expression using +// "with" on the argument value, and return the result. +function scopeExprToFunc(expr) { + /*jshint evil: true */ + var func = new Function("with (this) {return (" + expr + ");}"); + return function(scope) { + return func.call(scope); + }; +} + +function asArray(value) { + if (value === null) + 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'); +}; //--------------------------------------------------------------------- @@ -268,3941 +268,3931 @@ var browser = (function() { //--------------------------------------------------------------------- // Source file: ../srcjs/input_rate.js - var Invoker = function(target, func) { - this.target = target; - this.func = func; +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); - (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; - var Debouncer = function(target, func, delayMs) { - this.target = target; - this.func = func; - this.delayMs = delayMs; + this.timerId = null; + this.args = null; +}; - this.timerId = 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); - (function() { - this.normalCall = function() { - var self = this; +var Throttler = function(target, func, delayMs) { + this.target = target; + this.func = func; + this.delayMs = delayMs; - this.$clearTimer(); - this.args = arguments; + 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(); - self.$invoke(); + 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(Debouncer.prototype); - - var Throttler = function(target, func, delayMs) { - this.target = target; - this.func = func; - this.delayMs = delayMs; - - this.timerId = null; + } + }; + 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); - (function() { - this.normalCall = function() { - var self = this; +// 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); + }; +} - 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 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; - // 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() { + 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; - 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; } + 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 = []; +// 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); + } }; - (function() { - this.setInput = function(name, value) { - var self = this; +}).call(InputBatchSender.prototype); - 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 || {}; +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); }; - (function() { - this.setInput = function(name, value) { - var jsonValue = JSON.stringify(value); - if (this.lastSentValues[name] === jsonValue) - return; - this.lastSentValues[name] = jsonValue; + 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); - }; - 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 = {}; + else + this.pendingInput[name] = value; }; - (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 = {}; + this.submit = function() { + for (var name in this.pendingInput) { + if (this.pendingInput.hasOwnProperty(name)) + this.target.setInput(name, this.pendingInput[name]); + } }; - (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); - +}).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); + //--------------------------------------------------------------------- // Source file: ../srcjs/shinyapp.js - var ShinyApp = function() { - this.$socket = null; +var ShinyApp = function() { + this.$socket = null; - // Cached input values - this.$inputValues = {}; + // Cached input values + this.$inputValues = {}; - // Output bindings - this.$bindings = {}; + // Output bindings + this.$bindings = {}; - // Cached values/errors - this.$values = {}; - this.$errors = {}; + // Cached values/errors + this.$values = {}; + this.$errors = {}; - // Conditional bindings (show/hide element based on expression) - this.$conditionals = {}; + // Conditional bindings (show/hide element based on expression) + this.$conditionals = {}; - this.$pendingMessages = []; - this.$activeRequests = {}; - this.$nextRequestId = 0; + 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(); }; - (function() { + this.isConnected = function() { + return !!this.$socket; + }; - this.connect = function(initialInput) { - if (this.$socket) - throw "Connect was already called on this application object"; + this.createSocket = function () { + var self = this; - $.extend(initialInput, { - // IE8 and IE9 have some limitations with data URIs - ".clientdata_allowDataUriScheme": typeof WebSocket !== 'undefined' - }); + var createSocketFunc = exports.createSocket || function() { + var protocol = 'ws:'; + if (window.location.protocol === 'https:') + protocol = 'wss:'; - 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)) { + 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); - // Bizarrely, QtWebKit requires us to encode these characters *twice* - if (browser.isQt) { - defaultPath = encodeURI(defaultPath); - } } - if (!/\/$/.test(defaultPath)) - defaultPath += '/'; - defaultPath += 'websocket/'; + } + if (!/\/$/.test(defaultPath)) + defaultPath += '/'; + defaultPath += 'websocket/'; - var ws = new WebSocket(protocol + '//' + window.location.host + defaultPath); - ws.binaryType = 'arraybuffer'; - return ws; + 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 socket = createSocketFunc(); - socket.onopen = function() { - socket.send(JSON.stringify({ - method: 'init', - data: self.$initialInput - })); + var payload = []; + payload.push(uint32_to_buf(0x01020202)); // signature - 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; - }; + var jsonBuf = makeBlob([msg]); + payload.push(uint32_to_buf(jsonBuf.size)); + payload.push(jsonBuf); - 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); + for (var i = 0; i < blobs.length; i++) { + payload.push(uint32_to_buf(blobs[i].byteLength || blobs[i].size || 0)); + payload.push(blobs[i]); } - 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; + msg = makeBlob(payload); } - // 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.'); - } + this.$sendMsg(msg); + }; - customMessageHandlerOrder.push(type); - customMessageHandlers[type] = handler; + 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); } - exports.addCustomMessageHandler = addCustomMessageHandler; + return value; + }; - this.dispatchMessage = function(msg) { - var msgObj = JSON.parse(msg); + 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; - // Send msgObj.foo and msgObj.bar to appropriate handlers - this._sendMessagesToHandlers(msgObj, messageHandlers, messageHandlerOrder); + if (this.$values[id] !== undefined) + binding.onValueChange(this.$values[id]); + else if (this.$errors[id] !== undefined) + binding.onValueError(this.$errors[id]); - this.$updateConditionals(); - }; + return binding; + }; + this.unbindOutput = function(id, binding) { + if (this.$bindings[id] === binding) { + delete this.$bindings[id]; + return true; + } + else { + return false; + } + }; - // 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]; + this.$updateConditionals = function() { + var inputs = {}; - if (msgObj[msgType]) { - // Execute each handler with 'this' referring to the present value of - // 'this' - handlers[msgType].call(this, msgObj[msgType]); + // 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 handlers ===================================================== + // Message handler management functions ================================= - 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); + // 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); } - for (var key in message) { - if (message.hasOwnProperty(key)) - this.receiveOutput(key, message[key]); + // 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); } - }); - - addMessageHandler('errors', function(message) { - for (var key in message) { - if (message.hasOwnProperty(key)) - this.receiveError(key, message[key]); + if (typeof(message.detail) !== 'undefined') { + $progress.find('.progress-detail').text(message.detail); } - }); - - 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); + 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(); } } - }); - addMessageHandler('javascript', function(message) { - /*jshint evil: true */ - eval(message); - }); + $progress.fadeIn(); + }, - addMessageHandler('console', function(message) { - for (var i = 0; i < message.length; i++) { - if (console.log) - console.log(message[i]); - } - }); + // Close page-level progress bar + close: function(message) { + var $progress = $('#' + message.id + '.shiny-progress'); + $progress.removeClass('open'); - addMessageHandler('progress', function(message) { - if (message.type && message.message) { - var handler = progressHandlers[message.type]; - if (handler) - handler.call(this, message.message); - } - }); + $progress.fadeOut({ + complete: function() { + $progress.remove(); - 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); + // If this was the last shiny-progress, remove container + if ($('.shiny-progress').length === 0) + $('.shiny-progress-container').remove(); } + }); + }, - // 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); + // The 'bar' class is needed for backward compatibility with Bootstrap 2. + progressHTML: '