mirror of
https://github.com/rstudio/shiny.git
synced 2026-02-10 06:35:13 -05:00
399 lines
11 KiB
JavaScript
399 lines
11 KiB
JavaScript
(function() {
|
|
|
|
var $ = jQuery;
|
|
|
|
// 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() {
|
|
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;
|
|
}
|
|
|
|
function keyedFunc(delay, keyArgNum, func, modFunc) {
|
|
if (delay == 0)
|
|
return func;
|
|
|
|
var keyedFuncs = {};
|
|
return function() {
|
|
var key = keyArgNum == -1 ? this : arguments[keyArgNum];
|
|
if (!keyedFuncs[key])
|
|
keyedFuncs[key] = modFunc(delay, func);
|
|
keyedFuncs[key].apply(this, arguments);
|
|
};
|
|
}
|
|
|
|
// Behaves similarly to the debounce function, except that
|
|
// internally it will create a separate debounce function
|
|
// for each distinct value of one of the arguments. You
|
|
// specify which argument that is using keyArgNum (-1 means
|
|
// to use 'this').
|
|
//
|
|
// For example:
|
|
//
|
|
// function printNameValue(name, value) {
|
|
// console.log(name + '=' + value);
|
|
// }
|
|
// var debouncedPNV = keyedDebounce(500, 0, printNameValue);
|
|
// debouncedPNV('foo', 10);
|
|
// debouncedPNV('bar', 10);
|
|
// debouncedPNV('bar', 20);
|
|
//
|
|
// will print:
|
|
// foo=10
|
|
// bar=20
|
|
function keyedDebounce(delay, keyArgNum, func) {
|
|
return keyedFunc(delay, keyArgNum, func, debounce);
|
|
}
|
|
|
|
// Same as keyedDebounce, but for throttling
|
|
function keyedThrottle(delay, keyArgNum, func) {
|
|
return keyedFunc(delay, keyArgNum, func, throttle);
|
|
}
|
|
|
|
|
|
var ShinyApp = window.ShinyApp = function() {
|
|
this.$socket = null;
|
|
this.$bindings = {};
|
|
this.$values = {};
|
|
this.$pendingMessages = [];
|
|
};
|
|
|
|
(function() {
|
|
|
|
this.connect = function(initialInput) {
|
|
if (this.$socket)
|
|
throw "Connect was already called on this application object";
|
|
|
|
this.$socket = this.createSocket();
|
|
this.$initialInput = initialInput;
|
|
};
|
|
|
|
this.createSocket = function () {
|
|
var self = this;
|
|
|
|
var socket = new WebSocket('ws://' + window.location.host, 'shiny');
|
|
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');
|
|
};
|
|
return socket;
|
|
};
|
|
|
|
this.sendInput = function(values) {
|
|
var msg = JSON.stringify({
|
|
method: 'update',
|
|
data: values
|
|
});
|
|
|
|
if (this.$socket.readyState == WebSocket.CONNECTING) {
|
|
this.$pendingMessages.push(msg);
|
|
}
|
|
else {
|
|
this.$socket.send(msg);
|
|
}
|
|
};
|
|
|
|
this.receiveError = function(name, error) {
|
|
this.$values[name] = null;
|
|
|
|
var binding = this.$bindings[name];
|
|
if (binding && binding.onValueError) {
|
|
binding.onValueError(error);
|
|
}
|
|
}
|
|
|
|
this.receiveOutput = function(name, value) {
|
|
var oldValue = this.$values[name];
|
|
this.$values[name] = value;
|
|
if (oldValue === value)
|
|
return;
|
|
|
|
var binding = this.$bindings[name];
|
|
if (binding) {
|
|
binding.onValueChange(value);
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
this.dispatchMessage = function(msg) {
|
|
var msgObj = JSON.parse(msg);
|
|
for (key in msgObj.errors) {
|
|
this.receiveError(key, msgObj.errors[key]);
|
|
}
|
|
for (key in msgObj.values) {
|
|
for (name in this.$bindings)
|
|
this.$bindings[name].showProgress(false);
|
|
this.receiveOutput(key, msgObj.values[key]);
|
|
}
|
|
if (msgObj.progress) {
|
|
for (var i = 0; i < msgObj.progress.length; i++) {
|
|
var key = msgObj.progress[i];
|
|
var binding = this.$bindings[key];
|
|
if (binding && binding.showProgress) {
|
|
binding.showProgress(true);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.bind = 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;
|
|
return binding;
|
|
};
|
|
}).call(ShinyApp.prototype);
|
|
|
|
|
|
var LiveBinding = function(el) {
|
|
this.el = $(el);
|
|
};
|
|
(function() {
|
|
this.onValueChange = function(data) {
|
|
this.clearError();
|
|
this.renderValue(data);
|
|
};
|
|
this.onValueError = function(err) {
|
|
this.renderError(err);
|
|
};
|
|
this.renderError = function(err) {
|
|
this.el.text('ERROR: ' + err.message);
|
|
this.el.addClass('shiny-output-error');
|
|
};
|
|
this.clearError = function() {
|
|
this.el.removeClass('shiny-output-error');
|
|
};
|
|
this.showProgress = function(show) {
|
|
var RECALC_CLASS = 'recalculating';
|
|
if (show)
|
|
this.el.addClass(RECALC_CLASS);
|
|
else
|
|
this.el.removeClass(RECALC_CLASS);
|
|
};
|
|
}).call(LiveBinding.prototype);
|
|
|
|
|
|
var LiveTextBinding = function(el) {
|
|
LiveBinding.call(this, el);
|
|
};
|
|
(function() {
|
|
this.renderValue = function(data) {
|
|
this.el.text(data);
|
|
};
|
|
}).call(LiveTextBinding.prototype);
|
|
$.extend(LiveTextBinding.prototype, LiveBinding.prototype);
|
|
|
|
var LivePlotBinding = function(el) {
|
|
LiveBinding.call(this, el);
|
|
};
|
|
(function() {
|
|
this.renderValue = function(data) {
|
|
this.el.empty();
|
|
if (!data)
|
|
return;
|
|
var img = document.createElement('img');
|
|
img.src = data;
|
|
this.el.append(img);
|
|
};
|
|
}).call(LivePlotBinding.prototype);
|
|
$.extend(LivePlotBinding.prototype, LiveBinding.prototype);
|
|
|
|
var LiveHTMLBinding = function(el) {
|
|
LiveBinding.call(this, el);
|
|
};
|
|
(function() {
|
|
this.renderValue = function(data) {
|
|
this.el.html(data)
|
|
};
|
|
}).call(LiveHTMLBinding.prototype);
|
|
$.extend(LiveHTMLBinding.prototype, LiveBinding.prototype);
|
|
|
|
|
|
$(function() {
|
|
|
|
var shinyapp = window.shinyapp = new ShinyApp();
|
|
|
|
$('.shiny-text-output').each(function() {
|
|
shinyapp.bind(this.id, new LiveTextBinding(this));
|
|
});
|
|
$('.shiny-plot-output').each(function() {
|
|
shinyapp.bind(this.id, new LivePlotBinding(this));
|
|
});
|
|
$('.shiny-html-output').each(function() {
|
|
shinyapp.bind(this.id, new LiveHTMLBinding(this));
|
|
});
|
|
|
|
|
|
|
|
// Input bindings
|
|
|
|
// TODO: This all needs to be refactored to be more modular, extensible
|
|
|
|
function elementToValue(el) {
|
|
if (el.type == 'checkbox')
|
|
return el.checked ? true : false;
|
|
else
|
|
return $(el).val();
|
|
}
|
|
|
|
// Holds pending changes for deferred submit
|
|
var pendingData = {};
|
|
// Holds last sent data, so we know when we don't have to send again
|
|
var lastSentData = {};
|
|
|
|
var deferredSubmit = false;
|
|
$('input[type="submit"], button[type="submit"]').each(function() {
|
|
deferredSubmit = true;
|
|
$(this).click(function(event) {
|
|
event.preventDefault();
|
|
shinyapp.sendInput(pendingData);
|
|
$.extend(lastSentData, pendingData);
|
|
pendingData = {};
|
|
});
|
|
});
|
|
|
|
function onInputChange(name, value) {
|
|
if (lastSentData[name] === value) {
|
|
delete pendingData[name];
|
|
return;
|
|
}
|
|
|
|
if (deferredSubmit)
|
|
pendingData[name] = value;
|
|
else {
|
|
var data = {};
|
|
data[name] = value;
|
|
shinyapp.sendInput(data);
|
|
$.extend(lastSentData, data);
|
|
}
|
|
}
|
|
|
|
var debouncedInputChange = keyedDebounce(
|
|
deferredSubmit ? 0 : 250,
|
|
0,
|
|
onInputChange);
|
|
|
|
var inputSelector = ':input:not([type="submit"])';
|
|
var initialValues = {};
|
|
$(document).on('change keyup input', inputSelector, function() {
|
|
var input = this;
|
|
var name = input['data-input-id'] || input.name || input.id;
|
|
var value = elementToValue(input);
|
|
if (input.type == 'text')
|
|
debouncedInputChange(name, value);
|
|
else
|
|
onInputChange(name, value);
|
|
});
|
|
$(inputSelector).each(function() {
|
|
var input = this;
|
|
var name = input['data-input-id'] || input.name || input.id;
|
|
var value = elementToValue(input);
|
|
// TODO: validate name is non-blank, and no duplicates
|
|
// TODO: If submit button is present, don't send anything
|
|
// until submit button is pressed
|
|
initialValues[name] = value;
|
|
});
|
|
|
|
$('.shiny-plot-output').each(function() {
|
|
var width = this.offsetWidth;
|
|
var height = this.offsetHeight;
|
|
initialValues['.shinyout_' + this.id + '_width'] = width;
|
|
initialValues['.shinyout_' + this.id + '_height'] = height;
|
|
var self = this;
|
|
$(window).resize(debounce(500, function() {
|
|
if (self.offsetWidth != width) {
|
|
width = self.offsetWidth;
|
|
onInputChange('.shinyout_' + self.id + '_width', width);
|
|
}
|
|
if (self.offsetHeight != height) {
|
|
height = self.offsetHeight;
|
|
onInputChange('.shinyout_' + self.id + '_height', height);
|
|
}
|
|
}));
|
|
});
|
|
|
|
shinyapp.connect(initialValues);
|
|
lastSentData = initialValues;
|
|
});
|
|
})();
|