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: '
' + + '
' + + '
' + + 'message' + + '' + + '
' + + '
' + }; - // 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; + exports.progressHandlers = progressHandlers; - }).call(ShinyApp.prototype); +}).call(ShinyApp.prototype); //--------------------------------------------------------------------- // Source file: ../srcjs/file_processor.js - // Generic driver class for doing chunk-wise asynchronous processing of a - // FileList object. Subclass/clone it and override the `on*` functions to - // make it do something useful. - var FileProcessor = function(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; +// 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 + // TODO: Register error/abort callbacks - this.$run(); + this.$run(); +}; +(function() { + // Begin callbacks. Subclassers/cloners may override any or all of these. + this.onBegin = function(files, cont) { + setTimeout(cont, 0); }; - (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 + 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) + // 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; - - this.aborted = true; - this.onAbort(); + called = true; + self.$run(); }; + }; - // 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() { - // 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; - var self = this; + if (this.aborted || this.completed) + return; - 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 < 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 (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. - // 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); + var file = this.files[this.fileIndex++]; + this.onFile(file, this.$getRun()); + }; +}).call(FileProcessor.prototype); //--------------------------------------------------------------------- // Source file: ../srcjs/binding_registry.js - var BindingRegistry = function() { - this.bindings = []; - this.bindingNames = {}; +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; + } }; - (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); + 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(); +var inputBindings = exports.inputBindings = new BindingRegistry(); +var outputBindings = exports.outputBindings = new BindingRegistry(); //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding.js - var OutputBinding = exports.OutputBinding = function() {}; - (function() { - // Returns a jQuery object or element array that contains the - // descendants of scope that match this binding - this.find = function(scope) { throw "Not implemented"; }; +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.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); + this.onValueChange = function(el, data) { + this.clearError(el); + this.renderValue(el, data); + }; + this.onValueError = function(el, err) { + this.renderError(el, err); + }; + this.renderError = function(el, err) { + this.clearError(el); + if (err.message === '') { + // not really error, but we just need to wait (e.g. action buttons) + $(el).empty(); + return; + } + var errClass = 'shiny-output-error'; + if (err.type !== null) { + // use the classes of the error condition as CSS class names + errClass = errClass + ' ' + $.map(asArray(err.type), function(type) { + return errClass + '-' + type; + }).join(' '); + } + $(el).addClass(errClass).text(err.message); + }; + this.clearError = function(el) { + $(el).attr('class', function(i, c) { + return c.replace(/(^|\s)shiny-output-error\S*/g, ''); + }); + }; + this.showProgress = function(el, show) { + var RECALC_CLASS = 'recalculating'; + if (show) + $(el).addClass(RECALC_CLASS); + else + $(el).removeClass(RECALC_CLASS); + }; +}).call(OutputBinding.prototype); //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding_text.js - var textOutputBinding = new OutputBinding(); - $.extend(textOutputBinding, { - find: function(scope) { - return $(scope).find('.shiny-text-output'); - }, - renderValue: function(el, data) { - $(el).text(data); - } - }); - outputBindings.register(textOutputBinding, 'shiny.textOutput'); +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'); //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding_image.js - 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 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; + 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'); + // 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; + 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 opts = { - clickId: $el.data('click-id'), - clickClip: strToBool($el.data('click-clip')) || true, + var userX = devToUsrX(offset.x); + if (coordmap.log.x) + userX = Math.pow(10, userX); - dblclickId: $el.data('dblclick-id'), - dblclickClip: strToBool($el.data('dblclick-clip')) || true, - dblclickDelay: $el.data('dblclick-delay') || 400, + var userY = devToUsrY(offset.y); + if (coordmap.log.y) + userY = Math.pow(10, userY); - hoverId: $el.data('hover-id'), - hoverClip: $el.data('hover-clip') || true, - hoverDelayType: $el.data('hover-delay-type') || 'debounce', - hoverDelay: $el.data('hover-delay') || 300, + return { + x: userX, + y: userY + }; + } - 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, + // 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 + }; + } + } - coordmap: data.coordmap + // 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 }; - 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; - }); + if (offset.x > bounds.right) + newOffset.x = bounds.right; + else if (offset.x < bounds.left) + newOffset.x = bounds.left; - var $img = $(img); + if (offset.y > bounds.bottom) + newOffset.y = bounds.bottom; + else if (offset.y < bounds.top) + newOffset.y = bounds.top; - // 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 - }; - } + return newOffset; + } - // Transform offset coordinates to data space coordinates - function offsetToScaledCoords(offset, clip) { - // By default, clip to plotting region - clip = clip || true; + // 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; - 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; + return function(e) { + if (e === null) { + exports.onInputChange(inputId, null); + return; } - 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) { + // 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; - this.triggerPendingMousedown2(); - this.scheduleMousedown2(e); + // Object that encapsulates brush state + var brush = { + // Current brushing and dragging state + brushing: false, + dragging: false, - } 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); - } - } + // 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 }, - // 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); + // 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; } }; - $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); }); + // 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) + }; } - // ---------------------------------------------------------- - // Register the various event handlers - // ---------------------------------------------------------- - if (opts.clickId) { - var clickHandler = createClickHandler(opts.clickId); - $el.on('mousedown2.image_output', clickHandler.mousedown); + // 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]; - // When img is removed, do housekeeping: clear $el's mouse listener and - // call the handler's onRemoveImg callback. - $img.on('remove', clickHandler.onRemoveImg); + 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'); //--------------------------------------------------------------------- // Source file: ../srcjs/output_binding_html.js - 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 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); + 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; } - }); - outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput'); - var renderDependencies = exports.renderDependencies = function(dependencies) { - if (dependencies) { - $.each(dependencies, function(i, dep) { - renderDependency(dep); - }); - } - }; + exports.renderHtml(html, el, dependencies); + exports.initializeInputs(el); + exports.bindAll(el); + } +}); +outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput'); - // 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); - }; +var renderDependencies = exports.renderDependencies = function(dependencies) { + if (dependencies) { + $.each(dependencies, function(i, dep) { + renderDependency(dep); + }); + } +}; - var htmlDependencies = {}; - function registerDependency(name, version) { - htmlDependencies[name] = version; +// 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); +}; + +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); } - // 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 $("